# CDN API

Upload files to the FiveDash CDN from scripts with upload keys: endpoints, auth, formats, responses, errors and examples in cURL, Lua and JS.

Source: https://fivedash.fivex.gg/docs/en/developers/cdn-api

The FiveDash CDN keeps your city's images, videos and sounds on a fast public link. For scripts that upload files on their own (`screenshot-basic` screenshots, camera photos, clips), there's a simple API with **upload keys**: each key uploads files to one folder only. This page is that API's reference, for script developers.

## Before you start

* **Plan:** the **CDN** module must be in your plan, with the plan up to date. With the plan expired, the API answers `402`.
* **CDN created** on the server, under **CDN**. See [CDN](https://fivedash.fivex.gg/docs/en/cdn/overview).
* **Access level to create keys:** the CDN's **Settings** area, **Configure** level.
* **An upload key**, created in the panel (below). It starts with `fdm_` and is shown in full only once.

## Create an upload key

### Open the keys

Go to **CDN → Settings** and scroll to **Upload keys** (`/<org>/<server>/cdn/configuracoes`).

### New key

Click **New key**. In **New upload key**, give it a name (e.g. **Game camera**).

### Pick the folder

Under **Uploads to**, pick the **Key folder**. The key only uploads to that folder: if it leaks, the damage stays there.

### Allow deleting, if needed

Check **Can also delete** only if the script needs to clean up what it uploaded.

### Create and copy

Click **Create key**. In **Key created**, click **Copy key**. Afterwards, only the start and the end are shown. The same dialog has ready-made code in two tabs: **screenshot-basic (Lua)** and **HTTP (cURL)**, already with the right address.

The **Upload keys** list shows the name, the folder, **Can delete** (when it can), how many uploads the key made and when it was used. To cut a key off, use **Revoke**: scripts using it stop uploading right away, and files already uploaded stay. Each server can have up to 50 keys.

> Treat the key like a password. With `screenshot-basic`, it ships with the client-side script (players can see it), which is why it only uploads to one folder. Whenever you can, upload from the server. Never publish the key on GitHub or paste it in chats.

## Endpoints

Use the address shown in the **Key created** dialog. On the CDN's default address:

```http
https://cdn.fivex.sh/api/v1/upload
```

| Method   | Path                 | What it does                          | Key needs           |
| -------- | -------------------- | ------------------------------------- | ------------------- |
| `POST`   | `/api/v1/upload`     | Uploads a file to the key's folder    | any key             |
| `DELETE` | `/api/v1/files/<id>` | Deletes a file this same key uploaded | **Can also delete** |

There's no base64 upload or presigned URL for scripts: the upload is always a `multipart/form-data` with the file. (The large multipart uploads the panel does are panel-only.)

### Headers

| Header            | Required       | What it is                                                                                                                                        |
| ----------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Authorization`   | Yes            | `Bearer fdm_<64 hex characters>`                                                                                                                  |
| `Content-Type`    | Yes, on upload | `multipart/form-data; boundary=…` (cURL, `fetch` and `screenshot-basic` build it for you)                                                         |
| `Idempotency-Key` | No             | 8 to 128 characters: letters, numbers, `_`, `.` and `-`. Repeating the same operation with the same ID within 24 hours doesn't duplicate the file |

The key sets the server and the folder. You can't pick another folder in the form.

## Upload a file

`POST /api/v1/upload`

The form must have **a single field**, the file, named `file` (or `files[]`). Other fields make the upload be refused.

| Rule           | Value                                                                                                                                                                                                                 |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Files per call | 1                                                                                                                                                                                                                     |
| Size per call  | up to 20 MB (and never above the max size in the **Uploads** settings section)                                                                                                                                        |
| File name      | The name in the form, 1 to 180 characters, without `/`, `\`, `%`, `?`, `#` or leading/trailing spaces. If a file with that name already exists in the folder, `-1`, `-2`… is added before the extension               |
| File type      | The `Content-Type` of the file part, one of: `image/png`, `image/jpeg`, `image/webp`, `image/gif`, `image/svg+xml`, `video/mp4`, `video/webm`, `video/quicktime`, `audio/mpeg`, `audio/ogg`, `audio/wav`, `audio/mp4` |
| Content        | Must match the declared type. A `.png` that isn't a PNG is refused (`415`)                                                                                                                                            |
| Time           | The body must arrive within 2 minutes                                                                                                                                                                                 |

PNG, JPEG and WebP may be optimized on upload, following the **Images** settings section (quality, max width and metadata removal). GIF, SVG, audio and video stay as uploaded.

### Response

`200 OK`:

```json
{
  "ok": true,
  "id": "6f1d0c9e-2b7a-4c1e-9f3a-1a2b3c4d5e6f",
  "url": "https://cdn.fivex.sh/example-city/screenshots/print-0001.webp",
  "file": {
    "id": "6f1d0c9e-2b7a-4c1e-9f3a-1a2b3c4d5e6f",
    "name": "print-0001.webp",
    "folderId": "folder_example",
    "kind": "image",
    "mime": "image/webp",
    "size": 183204,
    "width": 1920,
    "height": 1080,
    "duration": null,
    "src": "https://cdn.fivex.sh/example-city/screenshots/print-0001.webp",
    "thumb": null,
    "optimizedSize": 120544,
    "uploadedBy": "Chave: Game camera",
    "createdAt": "2026-11-09T18:00:00.000Z",
    "starred": false,
    "trashedAt": null,
    "hits": 0
  }
}
```

| Field                | What it is                                                                                                  |
| -------------------- | ----------------------------------------------------------------------------------------------------------- |
| `id`                 | The file's ID. Keep it if the script will delete it later                                                   |
| `url`                | The file's public link. Always use this one, don't build the link by hand (the name may have gained a `-1`) |
| `file.kind`          | `image`, `video` or `audio`                                                                                 |
| `file.size`          | Bytes uploaded                                                                                              |
| `file.optimizedSize` | Bytes after optimization, when it happened                                                                  |
| `file.duration`      | Seconds, video and audio only                                                                               |
| `file.uploadedBy`    | `Chave: <key name>`                                                                                         |

The link follows `https://cdn.fivex.sh/<cdn-address>/<folders>/<name>`. With a custom domain (Ultra plan), the link uses your domain.

## Delete a file

`DELETE /api/v1/files/<id>`

Only works with a key that has **Can also delete**, and only reaches files this same key uploaded to its folder. The file is deleted right away, without going to the trash, and the link stops responding. Repeating the same `DELETE` with the same `Idempotency-Key` answers `{ "ok": true }` again.

```bash
curl -X DELETE "https://cdn.fivex.sh/api/v1/files/6f1d0c9e-2b7a-4c1e-9f3a-1a2b3c4d5e6f" \
  -H "Authorization: Bearer fdm_xxxxxxxx"
```

`200 OK` response:

```json
{ "ok": true }
```

## Errors

Errors come as `{ "ok": false, "code": "…", "message": "…" }`, with the message in Portuguese.

| Status | `code`            | Message                                                                                                         | When                                                         |
| ------ | ----------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `400`  | `invalid_request` | "Envie um formulário multipart válido."                                                                         | The body isn't `multipart/form-data`                         |
| `400`  | `invalid_request` | "Envie um único arquivo por formulário."                                                                        | More than one field in the form                              |
| `400`  | `invalid_request` | "Envie um arquivo no campo file."                                                                               | No file, empty file or larger than 20 MB                     |
| `400`  |                   | "Dados inválidos."                                                                                              | File type not accepted or invalid name                       |
| `400`  | `invalid_request` | "A chave de repetição é inválida."                                                                              | `Idempotency-Key` in the wrong format                        |
| `401`  | `invalid_request` | "Chave de envio inválida."                                                                                      | No `Authorization`, wrong format or a key that doesn't exist |
| `401`  | `invalid_request` | "A chave de envio foi revogada."                                                                                | The key was revoked in the panel                             |
| `402`  | `invalid_request` | "O plano da CDN venceu."                                                                                        | The CDN plan isn't active                                    |
| `403`  | `invalid_request` | "Essa chave não pode apagar arquivos."                                                                          | `DELETE` without **Can also delete**                         |
| `404`  | `invalid_request` | "A pasta da chave não está disponível."                                                                         | The key's folder was deleted or is in the trash              |
| `404`  | `invalid_request` | "Arquivo não encontrado."                                                                                       | `DELETE` of a file that isn't this key's or doesn't exist    |
| `405`  | `invalid_request` | "Operação não permitida."                                                                                       | A method or path that doesn't exist                          |
| `408`  | `invalid_request` | "O envio demorou demais. Tente novamente."                                                                      | The body took more than 2 minutes                            |
| `409`  | `invalid_request` | "A solicitação já foi usada com outros dados."                                                                  | Same `Idempotency-Key` with another file                     |
| `409`  | `invalid_request` | "A chave de repetição expirou. Use uma nova chave."                                                             | `Idempotency-Key` older than 24 hours                        |
| `413`  | `invalid_request` | "A solicitação é grande demais." / "O arquivo ultrapassa o tamanho permitido." / "Imagens podem ter até 20 MB." | File too large                                               |
| `415`  | `invalid_request` | "O conteúdo não corresponde ao formato informado."                                                              | The content isn't the declared type                          |
| `429`  | `quota_exceeded`  | "A franquia de armazenamento ou envios foi atingida."                                                           | The server hit the plan's limit                              |
| `429`  | `busy`            | "Há muitos envios em andamento. Tente novamente em instantes."                                                  | Too many uploads at once                                     |
| `503`  | `unavailable`     | "A CDN está indisponível. Tente novamente."                                                                     | Temporary failure                                            |

> To retry safely after a network error, `408`, `429` or `503`, repeat the same upload with the same `Idempotency-Key`. If the file had already been published, the response brings the same file, without a duplicate.

## Examples

### cURL

```bash
curl -X POST https://cdn.fivex.sh/api/v1/upload \
  -H "Authorization: Bearer fdm_xxxxxxxx" \
  -H "Idempotency-Key: print-20261109-0001" \
  -F "file=@print.png"
```

For video and audio, set the type on the file part (cURL doesn't detect all of them):

```bash
curl -X POST https://cdn.fivex.sh/api/v1/upload \
  -H "Authorization: Bearer fdm_xxxxxxxx" \
  -F "file=@clip.mp4;type=video/mp4"
```

### Lua with screenshot-basic (client)

The same code the panel shows in **Key created**:

```lua
exports['screenshot-basic']:requestScreenshotUpload('https://cdn.fivex.sh/api/v1/upload', 'file', {
  headers = { ['Authorization'] = 'Bearer fdm_xxxxxxxx' },
  encoding = 'webp'
}, function(data)
  local res = json.decode(data)
  print(res.url) -- https://cdn.fivex.sh/example-city/screenshots/print-….webp
end)
```

To record the screenshot in a log, send `res.url` to the server with one of your script's events and call the logs export there, with `image = url`.

### JavaScript on the server (FXServer)

In a JavaScript `server_script`, FXServer's Node 22 already has `fetch`, `FormData` and `Blob`. That way the key stays on the server:

```ts
// server.js — the key comes from a convar: set cdn_upload_key "fdm_xxxxxxxx"
const KEY = GetConvar("cdn_upload_key", "");

async function uploadToCdn(bytes, name, type) {
  const form = new FormData();
  form.append("file", new Blob([bytes], { type }), name);
  const response = await fetch("https://cdn.fivex.sh/api/v1/upload", {
    method: "POST",
    headers: { Authorization: `Bearer ${KEY}`, "Idempotency-Key": `upload-${name}` },
    body: form,
  });
  const result = await response.json();
  if (!result.ok) throw new Error(`${response.status} ${result.message}`);
  return result.url;
}

exports("uploadToCdn", uploadToCdn);
```

### JavaScript in the NUI

The API accepts calls from any origin (CORS open for `Authorization`, `Content-Type` and `Idempotency-Key`), so an NUI page can upload directly. Remember: the key is visible on the client.

```ts
// In the NUI: uploads a canvas as WebP
async function sendCanvas(canvas: HTMLCanvasElement, key: string) {
  const blob: Blob = await new Promise((resolve) => canvas.toBlob((b) => resolve(b!), "image/webp", 0.9));
  const form = new FormData();
  form.append("file", blob, `photo-${Date.now()}.webp`);
  const response = await fetch("https://cdn.fivex.sh/api/v1/upload", {
    method: "POST",
    headers: { Authorization: `Bearer ${key}` },
    body: form,
  });
  const result = await response.json();
  if (!result.ok) throw new Error(result.message);
  return result.url as string;
}
```

## Public links and protection

* CDN links are public: whoever has the link sees the file.
* With **Only the city uses the files** on (in the **Protection** section), other sites can't show your files. The game, the NUI and the loading screen always can.
* Size, traffic and uploads count toward your plan's limit. See [Usage and limits](https://fivedash.fivex.gg/docs/en/cdn/usage-and-limits).

## FAQ

**Can I upload to another folder with the same key?**
No. The folder belongs to the key. Create one key per folder (or per script).

**Can I upload base64?**
No. Convert it to bytes and send it as `multipart/form-data` in the `file` field.

**Does the file's link change if I rename the file or folder in the panel?**
Yes, the link follows the current path. If you change the CDN's **Address**, old links stop responding: update them in your scripts.

**I revoked the key. Do the files disappear?**
No. Files already uploaded stay. Only new uploads with that key are refused.

## Next steps

[Upload keys](https://fivedash.fivex.gg/docs/en/cdn/upload-keys)Create, track and revoke keys in the panel.
[Integrations](https://fivedash.fivex.gg/docs/en/cdn/integrations)screenshot-basic, inventories and loading screens.
[Usage and limits](https://fivedash.fivex.gg/docs/en/cdn/usage-and-limits)Your plan's space, traffic and uploads.
[Logs API](https://fivedash.fivex.gg/docs/en/developers/logs-api)Record the screenshot in a log with the CDN link.
