Media API
Upload files to your team's media via the API, list, tag, organize them in folders, and manage them.
Media API
With the Media API, game servers, bots, launchers, phone scripts, or your own tools upload files directly into your team's Media – and get a CDN link back right away for public files.
Basics
| What | Value |
|---|---|
| Base URL | https://api.team-panel.com |
| Routes | all under /v1/cdn/media/{teamId} |
| Authentication | Authorization: Bearer TP_… (team API key) |
| Rate limit | 32 requests / 10 seconds per API key |
| Public URLs | https://cdn.team-panel.com/media/team/<teamId>/<file> |
You find the team ID and the ready-made upload URL under Media in the API & Integrations tab.
Dedicated key for uploads
Create a dedicated API key with the media:upload scope for
game servers and scripts. It can then only use the two upload routes and can be rerolled or
deleted independently. The key additionally only has the permissions of its linked member (or the
owner) – uploads require team.media.upload.
Keep your key secret
Upload files server-side whenever possible. A key shipped to a client (e.g. in a game, an app, or the browser) can be extracted and abused.
Endpoints
All paths are relative to /v1/cdn/media/{teamId}:
| Method | Path | Description | Permission |
|---|---|---|---|
POST | /upload/file | Upload as multipart/form-data (game servers, curl) | team.media.upload |
POST | /upload | Upload as JSON with a data URI (server-side scripts) | team.media.upload |
GET | (base path) | List, filter, and search files | team.media.view |
GET | /{mediaId} | URL of a file (public or signed, valid for 1 h) | team.media.view |
PATCH | /update/{mediaId} | Change file name, visibility, folder, tags | team.media.edit |
DELETE | /delete/{mediaId} | Delete a file | team.media.delete |
POST | /bulk | Bulk action for up to 100 files | edit / delete |
GET | /folders | List folders | team.media.view |
POST | /folders | Create a folder | team.media.edit |
PATCH | /folders/{folderId} | Rename a folder or change its rules | team.media.edit |
DELETE | /folders/{folderId} | Delete a folder (contents and subfolders move to parent) | team.media.edit |
GET | /tags?search= | Tags in use with counts | team.media.view |
GET | /usage?days=30 | Storage, limits, and daily usage (1–90 days) | team.media.view |
Request and response schemas are available in the Swagger UI.
Upload (multipart)
POST /v1/cdn/media/{teamId}/upload/file
Send the file in the file form field – otherwise the first file in the form is used. Options as query parameters:
| Parameter | Description |
|---|---|
public | true / false. Default: true – or the target folder's default visibility |
fileName | Stored file name. Default: name of the uploaded file |
folder | Folder name or path, e.g. screenshots or screenshots/2026. Missing folders are created |
folderId | ID of an existing folder. Not together with folder |
tags | Comma-separated, e.g. license:abc123,type:screenshot |
Response 201:
{
"id": "4d6c…",
"path": "media/team/<teamId>/<file>.jpg",
"publicUrl": "https://cdn.team-panel.com/media/team/<teamId>/<file>.jpg",
"usedTeamStorage": 1234567,
"folderId": "8f1e…",
"tags": ["license:abc123", "type:screenshot"]
}For private files publicUrl is null – get the signed link via GET /{mediaId}.
curl -X POST "https://api.team-panel.com/v1/cdn/media/<teamId>/upload/file?folder=screenshots&tags=type:screenshot" \
-H "Authorization: Bearer TP_your_key" \
-F "file=@screenshot.jpg"// Node.js 18+ (fetch, FormData, and Blob are built in)
import { readFile } from 'node:fs/promises';
const TEAM_ID = process.env.TEAMPANEL_TEAM_ID;
const API_KEY = process.env.TEAMPANEL_API_KEY; // TP_…
const form = new FormData();
form.append(
'file',
new Blob([await readFile('screenshot.png')], { type: 'image/png' }),
'screenshot.png',
);
const url = new URL(`https://api.team-panel.com/v1/cdn/media/${TEAM_ID}/upload/file`);
url.searchParams.set('folder', 'screenshots');
url.searchParams.set('tags', 'type:screenshot,discord:123456789');
const res = await fetch(url, {
method: 'POST',
headers: { Authorization: `Bearer ${API_KEY}` },
body: form,
});
if (res.status !== 201) throw new Error(`Upload failed (${res.status}): ${await res.text()}`);
const { id, publicUrl } = await res.json();
console.log(id, publicUrl);Upload (JSON)
POST /v1/cdn/media/{teamId}/upload
Best for server-side scripts that already have the file as base64 or a data URI (e.g. FiveM with screenshot-basic). The response matches the multipart upload.
{
"file": "data:image/jpeg;base64,/9j/4AAQ…",
"fileName": "screenshot.jpg",
"public": true,
"folder": "screenshots",
"tags": ["license:abc123", "type:screenshot"]
}public, folder, folderId, and tags (an array here) are optional – with the same rules as above.
If public is omitted, the target folder's default visibility applies. Without a folder default, JSON uploads are private (false), whereas multipart uploads are public (true).
Complete FiveM example: FiveM screenshots.
List files
GET /v1/cdn/media/{teamId}
| Query | Description |
|---|---|
folderId | Folder ID or none (only files without a folder) |
includeSubfolders | true – also include files from subfolders of folderId |
tags | Comma-separated; a file must have all tags (AND) |
search | Search in the file name |
type | image, video, audio, document, or all |
Paging uses the pageNumber (starting at 1) and pageSize (default 50) headers. The response contains media, totalCount (total for the filters), and usedTeamStorage.
# All screenshots of one player, page 2 with 48 items each
curl "https://api.team-panel.com/v1/cdn/media/<teamId>?tags=license:abc123,type:screenshot" \
-H "Authorization: Bearer TP_your_key" \
-H "pageNumber: 2" \
-H "pageSize: 48"Get, update, delete a file
GET /{mediaId}→{ "url": "…" }– the CDN link for public files, a signed link (valid for 1 hour) for private filesPATCH /update/{mediaId}with{ "fileName"?, "public"?, "folderId"?, "tags"? }–folderId: nullremoves the folder,tagsreplaces all tagsDELETE /delete/{mediaId}– deletes the file and removes it from the CDN cache
Bulk actions
POST /v1/cdn/media/{teamId}/bulk – for up to 100 files:
action | Additional field | Permission |
|---|---|---|
delete | – | team.media.delete |
move | folderId (UUID or null) | team.media.edit |
setPublic | public (true / false) | team.media.edit |
addTags | tags (array) | team.media.edit |
removeTags | tags (array) | team.media.edit |
curl -X POST "https://api.team-panel.com/v1/cdn/media/<teamId>/bulk" \
-H "Authorization: Bearer TP_your_key" \
-H "Content-Type: application/json" \
-d '{ "action": "move", "ids": ["<mediaId>", "<mediaId>"], "folderId": "<folderId>" }'
# → { "affected": 2, "failed": [] }Files that could not be processed (e.g. IDs of another team or more than 20 tags) are listed in failed with id and message.
Folders
POST /folders creates a folder:
{
"name": "screenshots",
"parentId": null,
"retentionDays": 30,
"maxFiles": 500,
"defaultPublic": true
}| Field | Description |
|---|---|
name | 1–100 characters, must be unique (otherwise 409) |
parentId | Parent folder for subfolders (max. 5 levels) |
retentionDays | Automatically delete files after N days |
maxFiles | Keep only the newest N files |
defaultPublic | Default visibility for uploads into this folder |
Rules only apply to files directly in the folder and are enforced hourly. PATCH /folders/{folderId} accepts the same fields. On DELETE, files and subfolders move to the parent folder.
Tags & usage
GET /tags?search=license– most used tags with counts, e.g. for autocompleteGET /usage?days=30– storage, plan limits (null= unlimited), uploads per day (including API) and – when enabled – CDN requests and traffic. Sizes in bytes.
Tag rules: 1–64 characters a-z 0-9 _ . : @ -, lower case, max. 20 per file. We recommend key:value, e.g. license:…, steam:…, discord:…, type:screenshot.
Error codes
| Status | Meaning |
|---|---|
400 | Invalid input, invalid tag, more than 20 tags, invalid folder, or folder + folderId |
401 | Invalid API key or key of another team |
402 | Plan limit reached: file too large (maxMediaFileSize) or team storage full (maxTeamStorage) |
403 | Member is missing a permission or the key is missing a scope |
404 | File or folder not found |
409 | Folder name already taken |
413 | Request too large (multipart 260 MB, JSON 140 MB – files above ~100 MB via multipart only) |
415 | Blocked file type (HTML, JavaScript, executables, shell scripts, SVG) |
429 | Rate limit exceeded (32 requests / 10 s per key) |
503 | Upload capacity is temporarily busy (error.media.uploadBusy); Retry-After: 5 |
For 503 with error.media.uploadBusy, wait at least the time given in the Retry-After header (currently 5 seconds). Then send the same upload again. The API rejects this request before it processes the request body; no file was created. This error is separate from the rate limit (429).
Plan limits
Media use the shared team storage (together with member files, to-do attachments, and backups).
The maximum file size grows with the plan (Free 10 MB up to Enterprise 250 MB). GET /usage
returns the current values (storage.usedBytes, limitBytes, mediaBytes, maxFileSizeBytes).
Automation
Every upload fires the Media uploaded event – e.g. to post new screenshots to Discord. See Automation.
See also
- Media – UI, folder rules, permissions
- FiveM screenshots – upload with
screenshot-basic - API keys – scopes and member context