Skip to content
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

  • fd_live_… is an API token. You make it in the panel under Tokens and choose its scopes. Send it as Authorization: fd_live_… or Authorization: Bearer fd_live_…. It works for this API, for the logs API and, as a key pair, for the S3 endpoint.
  • fda_live_… is an agent key. It goes in server.cfg as set fivedock_key for the fivedock resource, and nowhere else. The upload API does not accept it.
curl
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.

Upload API addresses
AddressScopeWhat it does
Files
POST /api/v3/fileScope · files:writeUpload one file as multipart/form-data.
POST /api/v3/file/base64Scope · files:writeUpload one file sent as base64 in a JSON body.
POST /api/v3/file/archiveScope · files:writeUpload a .zip or .rar and store every file in it. FiveDock only.
GET /api/v3/fileScope · files:readList your files, a page at a time.
GET /api/v3/file/{id}Scope · files:readRead one file’s details.
DELETE /api/v3/file/{id}Scope · files:deleteDelete one file.
Upload links
GET /api/v3/file/presigned-urlScope · files:writeMint 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 credentialUpload through a link minted above, with no token.
GET /api/v2/presigned-urlScope · files:writeMint 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 credentialUpload through a link minted the older way, with no token.
Fivemanage v2
POST /api/v2/imageScope · files:writeUpload an image. The file goes in the image field or in file.
POST /api/v2/videoScope · files:writeUpload a video. The file goes in the video field or in file.
POST /api/v2/audioScope · files:writeUpload a sound. The file goes in the audio field or in file.
Fivemerr
POST /v1/media/imagesScope · files:writeUpload an image. The file goes in file or data.
POST /v1/media/videosScope · files:writeUpload a video. The file goes in file or data.
POST /v1/media/audiosScope · files:writeUpload a sound. The file goes in file or data.
Logs
POST /api/logsScope · logs:writeSend one log event.
POST /api/logs/batchScope · logs:writeSend log events in a batch.
POST /api/v3/logsScope · logs:writeSend 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.

Upload a file

Send POST /api/v3/file as multipart/form-data, with the file in the file part. Every other field is optional.

Upload fields
FieldWhat it does
fileThe file itself. One per request.
filenameThe name to store it under. Defaults to the name of the file part.
pathThe folder it lands in, such as /mdt/mugshots. Made on demand; the root by default.
keyA 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.
metadataA 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"}'

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.

URL
https://cdn.fivedock.dev/o/{workspace}/{key}
https://cdn.fivedock.dev/o/my-city/items/burger.png
  • A key is 1 to 512 characters, in segments split by /. No empty, . or .. segments, and no / at the start.
  • Letters, digits and . _ - ~ @ + : ( ) , = ! are allowed. A key with anything else, such as a space, is refused with 400.
  • Uploading to a key that is taken replaces the file behind the link. The old file is kept for 7 days, then purged, as after a delete.
  • Keyed links are sent with Cache-Control: public, max-age=300, stale-while-revalidate=60 and an ETag, so a replaced file reaches players within about 5 minutes and an unchanged one is revalidated with a cheap 304.
  • In the panel, a keyed file shows in its key’s folder: items/burger.png is under /items.
curl
# 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)
end

The 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.

JSON
{
  "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"
  }
}
  • url and data.url are the link to use. For a keyed upload, that is the stable /o/ link.
  • fivedock.fileUrl is the file’s own /f/ link. Its content never changes, and it is cached for a year.
  • fivedock.key is the key, when the file has one.
  • fivedock.deduplicated is true when the same bytes were already stored, so nothing new was billed.
  • fivedock.status is pending while optimized copies are being made, and ready once they are. The link works either way.

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
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.

JSON
{
  "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.

Upload link parameters
ParameterWhat it does
expiresAtWhen 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.
singleUsetrue 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.
maxSizeThe largest file the link accepts, in bytes, up to the per-file limit. A bigger file gets 413.
fileTypeimage, video or audio. Any other kind of file gets 415.
contentTypeOne exact type, such as video/webm. Any other type gets 415.
pathThe folder the file lands in.
keyStores the file under this key, so it gets a stable link and replaces what was there.
curl
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:

JSON
{
  "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.

curl
# 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_…"
JSON
{
  "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

  • One file can be up to 512 MiB.
  • A base64 JSON upload can carry up to 16 MiB of decoded file.
  • An archive can be up to 512 MiB, with at most 5,000 files and 4 GiB unpacked.
  • An upload request can take up to 5 minutes.

The type is read from the file’s bytes, not from its name or the Content-Type you send. These are accepted:

  • Images: PNG, JPEG, WebP, AVIF and GIF.
  • Video: MP4, WebM and QuickTime (.mov).
  • Audio: MP3, OGG and WAV.

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.

JSON
{
  "error": "that file type is not accepted",
  "message": "that file type is not accepted"
}
Error status codes
StatusWhen
400The request is malformed: no file part, base64 that does not decode, or a key that breaks the rules.
401No token, or a token that is not valid. An fda_live_… agent key gets this too.
403The token lacks the scope for this address, or the upload link is not valid or has expired.
404There is no file with that id in your workspace.
410The single-use link has already been used.
413The file is over the size limit, or over the link’s maxSize.
415The file type is not accepted or does not match the link’s fileType or contentType, or the archive is not a zip or rar.
422The 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.
429The workspace is over its storage cap. Back off, and honor Retry-After.
503That part of the API is not set up on the server you called.