On this page
Docs / Reference
Upload API reference
Every address the upload API answers, the fields it reads and what it sends back. It speaks Fivemanage’s dialect, so a script written for Fivemanage works once you change the base URL and the key.
Base URL and keys
Every address on this page is relative to https://api.fivedock.dev, and files are served from https://cdn.fivedock.dev.
Two keys, two jobs
curl "https://api.fivedock.dev/api/v3/file?limit=1" \
-H "Authorization: Bearer fd_live_…"Keep the API token on a server you control. Anything that runs in a player’s game or browser should upload through a one-time link instead, so it never sees the token.
Scopes
Each address needs one scope, shown in the table below, and a token without it gets 403. files:read lists and reads files, files:write uploads and mints upload links, files:delete deletes, folders:write makes folders, usage:read reads storage and traffic, and logs:write sends logs.
Addresses
The paths and field names are Fivemanage’s, plus an archive upload of FiveDock’s own. Fivemerr’s media addresses are answered too.
| Address | Scope | What it does |
|---|---|---|
| Files | ||
| POST /api/v3/file | Scope · files:write | Upload one file as multipart/form-data. |
| POST /api/v3/file/base64 | Scope · files:write | Upload one file sent as base64 in a JSON body. |
| POST /api/v3/file/archive | Scope · files:write | Upload a .zip or .rar and store every file in it. FiveDock only. |
| GET /api/v3/file | Scope · files:read | List your files, a page at a time. |
| GET /api/v3/file/{id} | Scope · files:read | Read one file’s details. |
| DELETE /api/v3/file/{id} | Scope · files:delete | Delete one file. |
| Upload links | ||
| GET /api/v3/file/presigned-url | Scope · files:write | Mint a one-time upload link. It lasts 15 minutes unless you ask for less or more. |
| POST /api/v3/file/presigned-url/{ticket} | Scope · None, the link is the credential | Upload through a link minted above, with no token. |
| GET /api/v2/presigned-url | Scope · files:write | Mint an upload link the older way. It lasts 5 minutes unless you ask for less or more. |
| POST /api/v2/presigned-url/{ticket} | Scope · None, the link is the credential | Upload through a link minted the older way, with no token. |
| Fivemanage v2 | ||
| POST /api/v2/image | Scope · files:write | Upload an image. The file goes in the image field or in file. |
| POST /api/v2/video | Scope · files:write | Upload a video. The file goes in the video field or in file. |
| POST /api/v2/audio | Scope · files:write | Upload a sound. The file goes in the audio field or in file. |
| Fivemerr | ||
| POST /v1/media/images | Scope · files:write | Upload an image. The file goes in file or data. |
| POST /v1/media/videos | Scope · files:write | Upload a video. The file goes in file or data. |
| POST /v1/media/audios | Scope · files:write | Upload a sound. The file goes in file or data. |
| Logs | ||
| POST /api/logs | Scope · logs:write | Send one log event. |
| POST /api/logs/batch | Scope · logs:write | Send log events in a batch. |
| POST /api/v3/logs | Scope · logs:write | Send logs to Fivemanage’s v3 address. |
The log addresses take the shapes Fivemanage clients send, including ox_lib’s Fivemanage logger (set ox:logger "fivemanage"), and need a token with logs:write. ox_lib keeps Fivemanage’s address in imports/logger/providers/fivemanage.lua, so change its endpoint line to FiveDock’s address, and again after each ox_lib update.
Upload a file
Send POST /api/v3/file as multipart/form-data, with the file in the file part. Every other field is optional.
| Field | What it does |
|---|---|
| file | The file itself. One per request. |
| filename | The name to store it under. Defaults to the name of the file part. |
| path | The folder it lands in, such as /mdt/mugshots. Made on demand; the root by default. |
| key | A stable name such as items/burger.png. The file is served at a link that never changes, and uploading to the same key again replaces it. When it is set, it wins over path and filename. |
| metadata | A JSON object kept with the file and sent back when you read it, such as the case or the character it belongs to. Anything that is not a JSON object is stored as an empty one. |
curl -X POST https://api.fivedock.dev/api/v3/file \
-H "Authorization: Bearer fd_live_…" \
-F "[email protected]" \
-F "path=/evidence/case-1042" \
-F 'metadata={"caseId":1042,"officer":"A. Nowak"}'Stable links with key
A file with a key is served under /o/, then your workspace’s short name, then the key. The short name never changes, and it is also the bucket name on the S3 endpoint.
https://cdn.fivedock.dev/o/{workspace}/{key}
https://cdn.fivedock.dev/o/my-city/items/burger.png# the first upload makes the link, the next ones replace what it serves
curl -X POST https://api.fivedock.dev/api/v3/file \
-H "Authorization: Bearer fd_live_…" \
-F "[email protected]" \
-F "key=items/burger.png"Upload base64
POST /api/v3/file/base64 takes a JSON body instead of a form. base64 is the file, as a data URI or plain base64, and filename, path, key and metadata work as they do in the form. It is the short way from Lua, because screenshot-basic already hands you a data URI.
-- server.lua: a mugshot for the MDT, sent as base64
-- your own convar with an API token (fd_live_…), not the fda_live_ agent key
local TOKEN = GetConvar('mdt_upload_token', '')
local function uploadMugshot(source, charId, cb)
exports['screenshot-basic']:requestClientScreenshot(source, { encoding = 'webp', quality = 0.9 }, function(err, dataUri)
if err then return cb(err) end
PerformHttpRequest('https://api.fivedock.dev/api/v3/file/base64', function(status, body)
if status ~= 200 then
return cb(('upload failed (%d): %s'):format(status, body or ''))
end
cb(nil, json.decode(body).url)
end, 'POST', json.encode({
base64 = dataUri,
filename = ('%s.webp'):format(charId),
key = ('mugshots/%s.webp'):format(charId),
metadata = { charId = charId },
}), {
['Authorization'] = TOKEN,
['Content-Type'] = 'application/json',
})
end)
endThe decoded file can be up to 16 MiB. Anything bigger goes as multipart, or through an upload link.
What an upload answers
Every upload address answers 200 with the same shape. The top-level fields are the ones Fivemanage sends; fivedock holds our extras.
{
"status": "ok",
"data": {
"id": "8Kq2nR4t",
"url": "https://cdn.fivedock.dev/o/my-city/mugshots/char1-abc.webp",
"originalUrl": "https://cdn.fivedock.dev/o/my-city/mugshots/char1-abc.webp"
},
"id": "8Kq2nR4t",
"uuid": "8Kq2nR4t",
"url": "https://cdn.fivedock.dev/o/my-city/mugshots/char1-abc.webp",
"filename": "char1-abc.webp",
"original_filename": "char1-abc.webp",
"mime_type": "image/webp",
"size": 48213,
"hash": "9f2b…",
"metadata": { "charId": "char1-abc" },
"created_at": "2026-09-29T12:00:00Z",
"updated_at": "2026-09-29T12:00:00Z",
"fivedock": {
"publicId": "8Kq2nR4t",
"key": "mugshots/char1-abc.webp",
"fileUrl": "https://cdn.fivedock.dev/f/8Kq2nR4t.webp",
"deduplicated": false,
"status": "ready",
"folder": "/mugshots",
"moderation": "approved"
}
}Upload an archive
POST /api/v3/file/archive takes a .zip or .rar in the file part and stores every file in it. path is the folder to unpack into, the root by default.
curl -X POST https://api.fivedock.dev/api/v3/file/archive \
-H "Authorization: Bearer fd_live_…" \
-F "[email protected]" \
-F "path=items" \
-F "keys=true"With keys=true, the default, every file becomes a keyed file, and its key is the target folder plus its path inside the archive: path=items and an entry burger.png give items/burger.png. With keys=false, files get random ids and land in the target folder plus their folder inside the archive.
Folders inside the archive are kept: an archive holding images/burger.png unpacked into items gives items/images/burger.png. To leave a folder out, zip what is inside it rather than the folder itself.
{
"status": "ok",
"data": {
"uploaded": [
{
"id": "Qw7tL2xa",
"key": "items/burger.png",
"url": "https://cdn.fivedock.dev/o/my-city/items/burger.png",
"fileUrl": "https://cdn.fivedock.dev/f/Qw7tL2xa.png",
"filename": "burger.png",
"path": "/items",
"size": 18233,
"mime_type": "image/png",
"deduplicated": false
}
],
"skipped": [{ "name": "readme.txt", "reason": "unsupported_type" }],
"incomplete": false
}
}Folders, links, __MACOSX/, hidden files, Thumbs.db and desktop.ini are skipped without a word. Any other file that cannot be stored is listed under skipped with a reason: unsupported_type, too_large, encrypted, unsafe_path, too_many_files, archive_too_large, storage_full, duplicate or failed. The answer is 200 when at least one file was stored, even if others were skipped; when none could be stored it is 422, with the same skipped list. If the archive is damaged partway through, incomplete is true: the files before the damage were stored and nothing after it was read.
The archive can be up to 512 MiB, with at most 5,000 files and 4 GiB unpacked, and each file has to be a type we accept. RAR 4 and RAR 5 are read; password-protected entries are skipped as encrypted, and multi-volume archives are not supported. Something that is not a zip or rar gets 415, and an archive that cannot be read gets 422. For bigger sets, sync over the S3 endpoint with rclone or the aws CLI.
One-time upload links
An upload link lets a player’s NUI, or any browser, upload a file without ever seeing your token. Your server mints it with GET /api/v3/file/presigned-url and a token with files:write, then hands it over. Every parameter is optional, and the uploader cannot change any of them.
| Parameter | What it does |
|---|---|
| expiresAt | When the link stops working, in unix seconds. The default is 15 minutes from now (5 on /api/v2/presigned-url), and the most is 1 hour. |
| singleUse | true makes the link accept exactly one successful upload; after that it answers 410 Gone. An attempt that is refused, for a wrong type or a file too large, does not use it up. |
| maxSize | The largest file the link accepts, in bytes, up to the per-file limit. A bigger file gets 413. |
| fileType | image, video or audio. Any other kind of file gets 415. |
| contentType | One exact type, such as video/webm. Any other type gets 415. |
| path | The folder the file lands in. |
| key | Stores the file under this key, so it gets a stable link and replaces what was there. |
curl "https://api.fivedock.dev/api/v3/file/presigned-url?singleUse=true&fileType=video&contentType=video/webm&maxSize=52428800&path=/mdt/videos" \
-H "Authorization: Bearer fd_live_…"The answer carries the link and repeats what it allows:
{
"status": "ok",
"data": {
"presignedUrl": "https://api.fivedock.dev/api/v3/file/presigned-url/eyJ…",
"expiresAt": "2026-09-29T12:15:00Z",
"singleUse": true,
"maxSize": 52428800,
"fileType": "video",
"contentType": "video/webm",
"path": "/mdt/videos"
}
}To upload, POST to presignedUrl as multipart/form-data, with the file in the file field and, if you like, filename and metadata. Send no Authorization header. The answer is the same as for POST /api/v3/file.
# no Authorization header: the link is the credential
curl -X POST "https://api.fivedock.dev/api/v3/file/presigned-url/eyJ…" \
-F "[email protected]"Recording a clip in NUI
A body-cam clip for an MDT: the server mints a single-use link for one webm file, the client script passes it to the NUI, and the NUI records with MediaRecorder and uploads the Blob straight to FiveDock.
-- server.lua: the token stays on the server
local TOKEN = GetConvar('mdt_upload_token', '') -- an API token (fd_live_…) with files:write
local function mintUploadLink(cb)
local query = ('singleUse=true&fileType=video&contentType=video/webm&maxSize=%d&expiresAt=%d&path=/mdt/videos')
:format(50 * 1024 * 1024, os.time() + 600)
PerformHttpRequest('https://api.fivedock.dev/api/v3/file/presigned-url?' .. query, function(status, body)
if status ~= 200 then return cb(nil) end
cb(json.decode(body).data.presignedUrl)
end, 'GET', '', { ['Authorization'] = TOKEN })
end
RegisterNetEvent('mdt:requestUploadLink', function()
local src = source
-- check that this player may attach evidence before minting anything
mintUploadLink(function(url)
TriggerClientEvent('mdt:uploadLink', src, url)
end)
end)
RegisterNetEvent('mdt:clipUploaded', function(url)
-- save url with the case: it plays straight away
end)Mint one link per upload and keep it short-lived. With singleUse=true, anyone who copies the link out of the NUI gets 410 once your clip is in.
List, read and delete
GET /api/v3/file lists your files, 50 to a page unless you set limit, which goes up to 100. page picks the page, type keeps only image, video or audio, and path keeps one folder. Add a file’s id to the address to read that file, or send DELETE to the same address to delete it. Each file comes with url and fileUrl: for a keyed file url is its stable link and key is listed too; otherwise both are the /f/ link.
# the second page of videos in /evidence
curl "https://api.fivedock.dev/api/v3/file?type=video&path=/evidence&page=2&limit=20" \
-H "Authorization: Bearer fd_live_…"
# one file
curl https://api.fivedock.dev/api/v3/file/8Kq2nR4t \
-H "Authorization: Bearer fd_live_…"
# delete it: the token needs files:delete
curl -X DELETE https://api.fivedock.dev/api/v3/file/8Kq2nR4t \
-H "Authorization: Bearer fd_live_…"{
"status": "ok",
"data": [
{
"id": "8Kq2nR4t",
"uuid": "8Kq2nR4t",
"filename": "evidence-3.webm",
"url": "https://cdn.fivedock.dev/f/8Kq2nR4t.webm",
"originalUrl": "https://cdn.fivedock.dev/f/8Kq2nR4t.webm",
"type": "video",
"mime_type": "video/webm",
"size": 4821377,
"hash": "c41d…",
"path": "/evidence",
"metadata": { "caseId": 1042 },
"created_at": "2026-09-29T12:00:00Z",
"updated_at": "2026-09-29T12:00:00Z"
}
],
"pagination": { "page": 2, "limit": 20, "total": 21, "pages": 2, "hasMore": false }
}A deleted file stops being served at once. For 7 days it can be restored from the panel. Deleting needs a token with files:delete.
Limits and file types
The type is read from the file’s bytes, not from its name or the Content-Type you send. These are accepted:
video/webm is accepted, so a MediaRecorder clip uploads as it is. The S3 endpoint also stores other types, and serves them as downloads.
Browsers and NUI (CORS)
The upload API answers browsers from any origin, with Access-Control-Allow-Origin: * and no cookies. That includes FiveM NUI pages, whose origin is https://cfx-nui- followed by the resource name. It covers /api/v3/file…, /api/v2/… and /v1/media/…. Preflight allows GET, POST and OPTIONS, with the Authorization and Content-Type headers.
CORS decides which pages may call, not who holds the key. Anything shipped to players can be read, so an NUI should upload through a one-time link and never hold a token. List and delete from your server.
When a link works
Straight away. The link in the answer serves as soon as the API has answered: files are approved on upload, so there is no queue to wait for. If moderation takes a file down later, its link answers 404.
A video plays as uploaded from the start. Optimized copies are added when processing finishes, and fivedock.status goes from pending to ready. You do not have to wait for that.
When you upload over a key, players get the new file within about 5 minutes, because keyed links are cached for 5 minutes. /f/ links never change content, so they are cached for a year.
Rate limits
There is no fixed per-token request limit on the upload API today. What stops uploads is the storage cap: when a workspace is over its storage limit with the hard cap on, uploads get 429 until storage is freed or the cap is raised. CDN delivery over the bandwidth cap answers 429 as well.
Back off on any 429, and when Retry-After is there, wait at least that long.
Errors
An error comes back with its status code and a JSON body; error and message say the same thing.
{
"error": "that file type is not accepted",
"message": "that file type is not accepted"
}| Status | When |
|---|---|
| 400 | The request is malformed: no file part, base64 that does not decode, or a key that breaks the rules. |
| 401 | No token, or a token that is not valid. An fda_live_… agent key gets this too. |
| 403 | The token lacks the scope for this address, or the upload link is not valid or has expired. |
| 404 | There is no file with that id in your workspace. |
| 410 | The single-use link has already been used. |
| 413 | The file is over the size limit, or over the link’s maxSize. |
| 415 | The file type is not accepted or does not match the link’s fileType or contentType, or the archive is not a zip or rar. |
| 422 | The file was taken down by moderation and cannot be uploaded again, the archive cannot be read, or no file in the archive could be stored. |
| 429 | The workspace is over its storage cap. Back off, and honor Retry-After. |
| 503 | That part of the API is not set up on the server you called. |