TeamPanel
API & Developers

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

WhatValue
Base URLhttps://api.team-panel.com
Routesall under /v1/cdn/media/{teamId}
AuthenticationAuthorization: Bearer TP_… (team API key)
Rate limit32 requests / 10 seconds per API key
Public URLshttps://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}:

MethodPathDescriptionPermission
POST/upload/fileUpload as multipart/form-data (game servers, curl)team.media.upload
POST/uploadUpload as JSON with a data URI (server-side scripts)team.media.upload
GET(base path)List, filter, and search filesteam.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, tagsteam.media.edit
DELETE/delete/{mediaId}Delete a fileteam.media.delete
POST/bulkBulk action for up to 100 filesedit / delete
GET/foldersList foldersteam.media.view
POST/foldersCreate a folderteam.media.edit
PATCH/folders/{folderId}Rename a folder or change its rulesteam.media.edit
DELETE/folders/{folderId}Delete a folder (contents and subfolders move to parent)team.media.edit
GET/tags?search=Tags in use with countsteam.media.view
GET/usage?days=30Storage, 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:

ParameterDescription
publictrue / false. Default: true – or the target folder's default visibility
fileNameStored file name. Default: name of the uploaded file
folderFolder name or path, e.g. screenshots or screenshots/2026. Missing folders are created
folderIdID of an existing folder. Not together with folder
tagsComma-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}

QueryDescription
folderIdFolder ID or none (only files without a folder)
includeSubfolderstrue – also include files from subfolders of folderId
tagsComma-separated; a file must have all tags (AND)
searchSearch in the file name
typeimage, 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 files
  • PATCH /update/{mediaId} with { "fileName"?, "public"?, "folderId"?, "tags"? } – folderId: null removes the folder, tags replaces all tags
  • DELETE /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:

actionAdditional fieldPermission
delete–team.media.delete
movefolderId (UUID or null)team.media.edit
setPublicpublic (true / false)team.media.edit
addTagstags (array)team.media.edit
removeTagstags (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
}
FieldDescription
name1–100 characters, must be unique (otherwise 409)
parentIdParent folder for subfolders (max. 5 levels)
retentionDaysAutomatically delete files after N days
maxFilesKeep only the newest N files
defaultPublicDefault 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 autocomplete
  • GET /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

StatusMeaning
400Invalid input, invalid tag, more than 20 tags, invalid folder, or folder + folderId
401Invalid API key or key of another team
402Plan limit reached: file too large (maxMediaFileSize) or team storage full (maxTeamStorage)
403Member is missing a permission or the key is missing a scope
404File or folder not found
409Folder name already taken
413Request too large (multipart 260 MB, JSON 140 MB – files above ~100 MB via multipart only)
415Blocked file type (HTML, JavaScript, executables, shell scripts, SVG)
429Rate limit exceeded (32 requests / 10 s per key)
503Upload 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

On this page