# API da CDN

Envie arquivos para a CDN do FiveDash a partir de scripts com chaves de envio: endpoints, autenticação, formatos, respostas, erros e exemplos em cURL, Lua e JS.

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

A CDN do FiveDash guarda as imagens, vídeos e sons da cidade num link público e rápido. Para scripts que mandam arquivos sozinhos (prints do `screenshot-basic`, fotos de câmera, clipes), existe uma API simples com **chaves de envio**: cada chave manda arquivos só para uma pasta. Esta página é a referência dessa API, para quem desenvolve scripts.

## Antes de começar

* **Plano:** o módulo **CDN** precisa estar no plano, com o plano em dia. Com o plano vencido, a API responde `402`.
* **CDN criada** no servidor, em **CDN**. Veja [CDN](https://fivedash.fivex.gg/docs/pt/cdn/overview).
* **Nível de acesso para criar chaves:** área **Configurações** da CDN, nível **Configurar** ("Mudar a otimização, a proteção e criar chaves").
* **Uma chave de envio**, criada no painel (abaixo). Ela começa com `fdm_` e aparece inteira uma vez só.

## Criar uma chave de envio

### Abra as chaves

Vá em **CDN → Configurações** e role até **Chaves de envio** (`/<org>/<servidor>/cdn/configuracoes`).

### Nova chave

Clique em **Nova chave**. Em **Nova chave de envio**, dê um nome (ex.: **Câmera do jogo**).

### Escolha a pasta

Em **Envia para**, escolha a **Pasta da chave**. A chave só envia para essa pasta: se vazar, o estrago fica ali.

### Permita apagar, se precisar

Marque **Também pode apagar** só se o script precisar limpar o que enviou.

### Crie e copie

Clique em **Criar chave**. Em **Chave criada**, clique em **Copiar chave**. Depois disso, só aparecem o começo e o fim. O mesmo diálogo traz o código pronto em duas abas: **screenshot-basic (Lua)** e **HTTP (cURL)**, já com o endereço certo.

Na lista de **Chaves de envio** aparecem o nome, a pasta, **Também apaga** (quando pode apagar), quantos envios a chave fez e quando foi usada. Para cortar uma chave, use **Revogar**: os scripts com ela param de enviar na hora, e os arquivos já enviados continuam. Cada servidor pode ter até 50 chaves.

> Trate a chave como senha. No `screenshot-basic`, ela vai junto do script no cliente (os jogadores conseguem vê-la), e é por isso que ela só envia para uma pasta. Sempre que der, envie pelo servidor. Nunca publique a chave no GitHub nem cole em chats.

## Endpoints

Use o endereço que aparece no diálogo **Chave criada**. No endereço padrão da CDN:

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

| Método   | Caminho              | O que faz                                    | Chave precisa de       |
| -------- | -------------------- | -------------------------------------------- | ---------------------- |
| `POST`   | `/api/v1/upload`     | Envia um arquivo para a pasta da chave       | qualquer chave         |
| `DELETE` | `/api/v1/files/<id>` | Apaga um arquivo que essa mesma chave enviou | **Também pode apagar** |

Não existe envio em base64 nem URL pré-assinada para scripts: o envio é sempre um `multipart/form-data` com o arquivo. (Os envios grandes em partes que o painel faz são só do painel.)

### Cabeçalhos

| Cabeçalho         | Obrigatório    | O que é                                                                                                                               |
| ----------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `Authorization`   | Sim            | `Bearer fdm_<64 caracteres hexadecimais>`                                                                                             |
| `Content-Type`    | Sim, no upload | `multipart/form-data; boundary=…` (o cURL, o `fetch` e o `screenshot-basic` montam sozinhos)                                          |
| `Idempotency-Key` | Não            | De 8 a 128 caracteres: letras, números, `_`, `.` e `-`. Repetir a mesma operação com o mesmo ID em até 24 horas não duplica o arquivo |

A chave define o servidor e a pasta. Não dá para escolher outra pasta no formulário.

## Enviar um arquivo

`POST /api/v1/upload`

O formulário precisa ter **um único campo**, o arquivo, com o nome `file` (ou `files[]`). Outros campos fazem o envio ser recusado.

| Regra                | Valor                                                                                                                                                                                                                     |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Arquivos por chamada | 1                                                                                                                                                                                                                         |
| Tamanho por chamada  | até 20 MB (e nunca acima do tamanho máximo da seção **Envios** das configurações)                                                                                                                                         |
| Nome do arquivo      | O nome que vem no formulário, de 1 a 180 caracteres, sem `/`, `\`, `%`, `?`, `#` nem espaços nas pontas. Se já existe um arquivo com esse nome na pasta, entra `-1`, `-2`… antes da extensão                              |
| Tipo do arquivo      | O `Content-Type` da parte do arquivo, um destes: `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` |
| Conteúdo             | Precisa bater com o tipo informado. Um `.png` que não é PNG é recusado (`415`)                                                                                                                                            |
| Tempo                | O corpo precisa chegar em até 2 minutos                                                                                                                                                                                   |

PNG, JPEG e WebP podem ser otimizados no envio, conforme a seção **Imagens** das configurações (qualidade, largura máxima e remoção de metadados). GIF, SVG, áudio e vídeo ficam como foram enviados.

### Resposta

`200 OK`:

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

| Campo                | O que é                                                                                            |
| -------------------- | -------------------------------------------------------------------------------------------------- |
| `id`                 | O ID do arquivo. Guarde se o script for apagar depois                                              |
| `url`                | O link público do arquivo. Use sempre este, não monte o link na mão (o nome pode ter ganhado `-1`) |
| `file.kind`          | `image`, `video` ou `audio`                                                                        |
| `file.size`          | Bytes enviados                                                                                     |
| `file.optimizedSize` | Bytes depois da otimização, quando ela aconteceu                                                   |
| `file.duration`      | Segundos, só para vídeo e áudio                                                                    |
| `file.uploadedBy`    | `Chave: <nome da chave>`                                                                           |

O link segue o formato `https://cdn.fivex.sh/<endereço-da-cdn>/<pastas>/<nome>`. Com domínio próprio (plano Ultra), o link usa o seu domínio.

## Apagar um arquivo

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

Só funciona com uma chave que tem **Também pode apagar**, e só alcança arquivos que essa mesma chave enviou para a pasta dela. O arquivo é apagado na hora, sem passar pela lixeira, e o link para de responder. Repetir o mesmo `DELETE` com a mesma `Idempotency-Key` responde `{ "ok": true }` de novo.

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

Resposta `200 OK`:

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

## Erros

Os erros vêm como `{ "ok": false, "code": "…", "message": "…" }`, com a mensagem em português.

| Status | `code`            | Mensagem                                                                                                        | Quando                                                      |
| ------ | ----------------- | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `400`  | `invalid_request` | "Envie um formulário multipart válido."                                                                         | O corpo não é `multipart/form-data`                         |
| `400`  | `invalid_request` | "Envie um único arquivo por formulário."                                                                        | Mais de um campo no formulário                              |
| `400`  | `invalid_request` | "Envie um arquivo no campo file."                                                                               | Sem arquivo, arquivo vazio ou maior que 20 MB               |
| `400`  |                   | "Dados inválidos."                                                                                              | Tipo de arquivo não aceito ou nome inválido                 |
| `400`  | `invalid_request` | "A chave de repetição é inválida."                                                                              | `Idempotency-Key` fora do formato                           |
| `401`  | `invalid_request` | "Chave de envio inválida."                                                                                      | Sem `Authorization`, formato errado ou chave que não existe |
| `401`  | `invalid_request` | "A chave de envio foi revogada."                                                                                | A chave foi revogada no painel                              |
| `402`  | `invalid_request` | "O plano da CDN venceu."                                                                                        | O plano da CDN não está ativo                               |
| `403`  | `invalid_request` | "Essa chave não pode apagar arquivos."                                                                          | `DELETE` sem **Também pode apagar**                         |
| `404`  | `invalid_request` | "A pasta da chave não está disponível."                                                                         | A pasta da chave foi apagada ou está na lixeira             |
| `404`  | `invalid_request` | "Arquivo não encontrado."                                                                                       | `DELETE` de um arquivo que não é dessa chave ou não existe  |
| `405`  | `invalid_request` | "Operação não permitida."                                                                                       | Método ou caminho que não existe                            |
| `408`  | `invalid_request` | "O envio demorou demais. Tente novamente."                                                                      | O corpo levou mais de 2 minutos                             |
| `409`  | `invalid_request` | "A solicitação já foi usada com outros dados."                                                                  | Mesma `Idempotency-Key` com outro arquivo                   |
| `409`  | `invalid_request` | "A chave de repetição expirou. Use uma nova chave."                                                             | `Idempotency-Key` com mais de 24 horas                      |
| `413`  | `invalid_request` | "A solicitação é grande demais." / "O arquivo ultrapassa o tamanho permitido." / "Imagens podem ter até 20 MB." | Arquivo grande demais                                       |
| `415`  | `invalid_request` | "O conteúdo não corresponde ao formato informado."                                                              | O conteúdo não é do tipo informado                          |
| `429`  | `quota_exceeded`  | "A franquia de armazenamento ou envios foi atingida."                                                           | O servidor chegou no limite do plano                        |
| `429`  | `busy`            | "Há muitos envios em andamento. Tente novamente em instantes."                                                  | Muitos envios ao mesmo tempo                                |
| `503`  | `unavailable`     | "A CDN está indisponível. Tente novamente."                                                                     | Falha temporária                                            |

> Para tentar de novo com segurança depois de um erro de rede, `408`, `429` ou `503`, repita o mesmo envio com a mesma `Idempotency-Key`. Se o arquivo já tinha sido publicado, a resposta traz o mesmo arquivo, sem duplicar.

## Exemplos

### 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"
```

Para vídeo e áudio, informe o tipo na parte do arquivo (o cURL não descobre todos sozinho):

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

### Lua com screenshot-basic (cliente)

O mesmo código que o painel mostra em **Chave criada**:

```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/cidade-exemplo/prints/print-….webp
end)
```

Para registrar o print num log, mande o `res.url` para o servidor com um evento do seu script e chame o export de logs lá, com `image = url`.

### JavaScript no servidor (FXServer)

Num `server_script` em JavaScript, o Node 22 do FXServer já tem `fetch`, `FormData` e `Blob`. Assim a chave fica no servidor:

```ts
// server.js — a chave vem de uma 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 na NUI

A API aceita chamadas de qualquer origem (CORS liberado para `Authorization`, `Content-Type` e `Idempotency-Key`), então uma página NUI pode enviar direto. Lembre: a chave fica visível no cliente.

```ts
// Na NUI: envia um canvas como 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, `foto-${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;
}
```

## Links públicos e proteção

* Os links da CDN são públicos: quem tem o link vê o arquivo.
* Com **Só a cidade usa os arquivos** ligado (seção **Proteção**), outros sites não conseguem mostrar seus arquivos. O jogo, a NUI e a loading screen sempre podem.
* Tamanho, tráfego e envios contam no limite do plano. Veja [Uso e limites](https://fivedash.fivex.gg/docs/pt/cdn/usage-and-limits).

## Perguntas frequentes

**Posso enviar para outra pasta com a mesma chave?**
Não. A pasta é da chave. Crie uma chave por pasta (ou por script).

**Posso enviar em base64?**
Não. Converta para bytes e envie como `multipart/form-data` no campo `file`.

**O link do arquivo muda se eu renomear o arquivo ou a pasta no painel?**
Sim, o link segue o caminho atual. Se você mudar o **Endereço** da CDN, os links antigos param de responder: troque nos scripts.

**Revoguei a chave. Os arquivos somem?**
Não. Os arquivos já enviados continuam. Só os envios novos com aquela chave são recusados.

## Próximos passos

[Chaves de envio](https://fivedash.fivex.gg/docs/pt/cdn/upload-keys)Crie, acompanhe e revogue chaves no painel.
[Integrações](https://fivedash.fivex.gg/docs/pt/cdn/integrations)screenshot-basic, inventários e loading screens.
[Uso e limites](https://fivedash.fivex.gg/docs/pt/cdn/usage-and-limits)Espaço, tráfego e envios do plano.
[API de logs](https://fivedash.fivex.gg/docs/pt/developers/logs-api)Registre o print num log com o link da CDN.
