Endpoints
GET /api/content/{type} list ?page=1&limit=1000&mine=1&…
POST /api/content/{type} create one object, or an array (≤ 25)
PUT /api/content/{type} bulk update array of { id, … } (≤ 25)
DELETE /api/content/{type} bulk delete [1,2,3] or { "ids": [1,2,3] } (≤ 100)
GET /api/content/{type}/{id} read one
PUT /api/content/{type}/{id} update one (partial)
DELETE /api/content/{type}/{id} delete one
Listing
/api/content/{type}API keycanReadList items of a type.
page defaults to 1. limit defaults to api.list.limitDefault (25) and is
clamped — up to 1, and down to api.list.limitMax (100) rather than
refused. Both are operator settings, and the ceiling is per role, so an
operator can give one role a larger page without a code change. Ask for what you
want and read what comes back; pagination.total is the full match count either
way.
?mine=1 restricts a list to rows you own — and is the only way to list your
own hidden items. Otherwise lists exclude hidden rows for non-staff.
Filters
Content-item lists accept these optional query parameters, and only the ones the type supports are applied:
| Param | Example | Meaning |
|---|---|---|
search |
?search=rust |
Name or title contains, case-insensitively |
tags |
?tags=pvp,vanilla |
Has any of these tag names |
categoryIds |
?categoryIds=3,7 |
In any of these categories |
communityId |
?communityId=12 |
Belongs to this community |
nsfw |
?nsfw=0 |
NSFW flag |
List responses are cached for api.list.cacheSec (30 seconds by default).
Creating
/api/content/{type}API keycanWriteOne object, or an array of up to 25.
curl -X POST https://api.moddingcommunity.com/api/content/asset \
-H "Authorization: Bearer $TMC_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "My Tool",
"description": "Does a thing",
"hidden": true,
"tags": ["tooling", "gamedev"],
"media": [{ "externalUrl": "https://example.com/shot.png", "title": "Screenshot" }]
}'
Fields
id and ownerId / userId are rejected — the owner column is injected
from the key’s user, and the id comes from the URL. Schemas are strict, so a
misspelled field is a 400 rather than a silent no-op.
A few fields are accepted by the schema and then resolved against your role:
isOfficial, the backdating pair createdAt / lastEdit, primary on groups
and a community’s flagged state. A key that is not staff cannot set them.
Everywhere
Every content item takes this set, so the per-type tables below only name what is particular to that type:
| Field | |
|---|---|
url |
The slug. Changing it leaves a redirect behind |
description, content |
The card blurb and the body |
hidden |
A draft. Behaves as if it does not exist to everyone else |
delist |
Unlisted, but not hidden — out of the browsers, carousels, search and the sitemap, while keeping its page and everything attached to it |
nsfw |
|
usesAi |
Self-declared “contains or was made with AI”. Disclosure only — it filters nothing and gates nothing |
archived |
Not on articles or collections |
apiPublic |
Anonymous read access through this API. On by default — see Reading without a key |
allowRatings, allowReviews, allowComments, allowMedia |
The engagement toggles |
bannerId, iconId, cardId |
File ids. Servers spell these banner / icon / card |
categoryIds |
The taxonomy set. Replaces the whole set when present |
tags |
Tag names, up to 100. undefined leaves them; [] clears them |
`categoryId` and `categoryIds` are different fields
categoryId is the single, often auto-detected category a type may carry;
categoryIds is the owner’s multi-select, and it replaces the whole set. Both
exist because the edit form posts both. Groups have only categoryIds — a
categoryId on a group used to be accepted and silently did nothing.
Per type
| Type | Required on create | Also accepts |
|---|---|---|
asset |
name |
appId, communityId, categoryId, environment, license, virusScanLink, subDisabled, media, releases, sourceItems |
mod |
name, content, appId |
install, communityId, categoryId, environment, license, virusScanLink, subDisabled, media, releases, links, sourceItems, redirect |
server |
appId |
See servers below |
community |
name |
ageRequirement, appIds, categoryId, media, links |
article |
title, content |
appId, communityId, categoryId, modId, serverId, assetId, userTargetId, media |
collection |
name |
items, includeItemMedia, subDisabled |
group |
name |
inviteOnly, appIds, appId, articleId, collectionId, communityId, modId, serverId, assetId, serverMapId |
Three spelling traps
Articles use title where every other type uses name.
Servers spell their image relations icon / banner / card; every other
type uses iconId / bannerId / cardId.
The inline sources array is sourceItems, not sources — the relation
sub-resource is the one called sources.
A group’s appId, modId, serverId and the rest say what the group is
about; appIds is the separate many-to-many of games it covers. They are not
the same field and setting one does not set the other.
Servers are an allow-list in three tiers
A server is the one type the API picks fields from rather than omitting them. Its input is shared with the query scanner and carries all of that scanner’s telemetry, so a new scanner field can never leak onto the public surface by being added upstream.
- Ordinary owner fields
name,description,content,rules,srcUrl,url, the address (ip4,ip6,port,portQuery,hostName,useHostName),countryId,appId,communityId,showNetInfo,showUsers,showVars,showSlideshow,icon,banner,card,tags,media,links,releases.- Descriptive facts the owner may state
gameMode,version,password,secure,os,dedicated. The edit form has always rendered these, and none of them moves a server up a ranking: correcting a mis-detected operating system is legitimate.- Gated telemetry
online,curUsers,maxUsers,bots,avgUsers,vars,mapandusers. Accepted by the schema and then checked, at request time, against this server’s own unlocks — because the answer depends on the row, not on the payload.
Telemetry is refused unless the server has unlocked it
Player counts, the online flag, the current map, the roster and the raw rule
dump are measured, not asserted — the API refuses them for everybody, the
owner and staff included. A server able to type its own curUsers would outrank
every honest one for free.
There are exactly two ways to be trusted with one: an integration on the
server holding SERVER_STATS / SERVER_USERS, or the owner turning on the
matching query override, which takes the field over from the scanner. The
refusal names the fields it refused and both routes. See Reporting your own
statistics.
Never writable at all, by anybody: lastScanned, retryCnt, locLat /
locLon, isOfficial and the scanner’s detection cursors. They are internal
bookkeeping with no owner-facing meaning, and no unlock makes them writable.
A server’s read is the opposite of its write, deliberately: everything we
know about a server is public on its page, so GET /api/content/server/{id}
resolves the map, the country, the categories, the rule set and the roster (up
to 512 rows) rather than making you scrape the page for the current map. The
owner’s showUsers and showVars switches still apply to the last two; the
owner and staff always see both.
Updating
/api/content/{type}/{id}API keycanWriteA partial update — omitted scalars keep their stored value.
Relation arrays are the exception. An inline relation array is always the
complete set: a PUT with no media key leaves media alone, but one with
"media": [ … ] replaces the gallery wholesale. There is no way to say “add
one” inline — use the relation sub-resource.
Bulk semantics
Bulk create and update validate and authorize every element before writing any of them, so a typo in the last item cannot leave the first nine half-created.
The writes themselves run sequentially and are not wrapped in a transaction: the canonical writers fan out into relation sync and cache invalidation of their own. If a write fails partway through, the response reports the failure and returns what did land:
{ "error": "…", "index": 3, "data": [ /* the items created before the failure */ ] }
Bulk delete authorizes the whole batch first and deletes nothing if any id is off-limits.