GET /api/content/<type> and GET /api/content/<type>/<id> answer with no
Authorization header at all, for items that are completely public.
curl "https://api.moddingcommunity.com/api/content/mod/123"
Nothing else on this surface does — every write, every relation, and every read of a non-content type still needs a key.
Three gates, all of which must pass
- The site allows it
A single site-wide switch (
api.anon.enabled).- The item is completely public
Not hidden (a draft or a moderation hold), not NSFW, and not filed under a hidden parent — the same rule the site’s own listings apply.
- The item's team has not opted out
apiPublicon the item, on by default and editable from its edit form. It governs this surface only: an authenticated key belonging to somebody who can already see the item is unaffected.
What answers
The seven content items: asset, mod, server, community, article,
collection, group.
The relation types — comment, review, media, release, tag, favorite,
filter — are not covered. A comment is a person’s words attached to an
account, and neither it nor a favourite is “basic information about a content
item”.
What comes back
A summary, not the record:
{
"data": {
"type": "mod",
"id": 123,
"name": "Better Vehicles",
"slug": "better-vehicles",
"path": "/gmod/m/better-vehicles",
"url": "https://moddingcommunity.com/gmod/m/better-vehicles",
"description": "…",
"createdAt": "2026-01-04T10:00:00.000Z",
"updatedAt": "2026-08-19T22:13:04.000Z",
"official": false,
"archived": false,
"app": { "id": 4, "name": "Garry's Mod", "slug": "gmod" },
"categories": [{ "id": 9, "name": "Vehicles", "slug": "vehicles" }],
"tags": [{ "id": 812, "name": "vehicles" }],
"stats": {
"views": 10432,
"favorites": 218,
"likes": 190,
"dislikes": 4,
"comments": 61
}
}
}
path is site-relative. url is the same address made absolute, and is null
on a deployment that has not configured its own origin — this endpoint has no
other honest source for it, since answers are cached under a key with no origin
in it and the first caller’s Host would otherwise be served to everybody
afterwards. path is null only when the item genuinely cannot be placed: an
article belonging to nothing but its author, whose URL is built from the owner
this surface does not publish.
tags is the item’s tag chips, capped at twelve and never a hidden or NSFW
one. A tag has no separate slug — name is unique and is what the site’s own
/blog/tag/<name> links interpolate, so it is both label and key. The field is
absent, rather than an empty array, when the item has none.
An article additionally carries image: the absolute CDN URL of its card
image, chosen by the same fallback chain the site’s own cards walk — the
article’s card, then its banner, then its single category’s art, then its app’s,
then its icon. Private uploads are skipped rather than published, because the
bucket would refuse them, so a fallback wins instead. The field is absent when
nothing in that chain is servable, or when the deployment has no CDN configured.
The other types have the same chains and can adopt it the same way; today only
article publishes one.
Servers carry a live block
Present only for server, and its address fields are populated only when that
server publishes them — the same showNetInfo switch that blurs the address
on its page. Otherwise every one of them is null.
"server": {
"online": true,
"players": 12,
"maxPlayers": 64,
"bots": 0,
"map": "Procedural",
"gameMode": "ffa",
"version": "1.0",
"password": false,
"secure": true,
"lastOnline": "2026-08-19T22:13:04.000Z",
"address": "203.0.113.7",
"port": 27015,
"connect": "203.0.113.7:27015"
}
connect is the one string somebody pastes into a console, built by the same
helper the site’s own copy button uses — IPv6 comes back bracketed
([2001:db8::1]:27015), and a server presenting as a hostname comes back as
that hostname.
It is kept alongside address and port rather than replacing them: a
caller assembling a connect URL wants the parts, and a caller displaying an
address wants the label.
What is deliberately absent
- The owner
A member has three separate anonymity switches, and an endpoint that answered “who made this” would have to re-derive all of them and get it wrong once.
- Anything from a relation table except a count, a tag chip or a card image
Media, files, releases, the roster. Those are the expensive half, and they are what a key is for. Tags and the card image are the exceptions because they are what a caller rendering the item needs, and neither is a payload: a tag is a public label, and the image is one URL.
- Engagement counts on the LIST endpoint
A page of twenty items would be a hundred count queries for numbers nobody asked for at that altitude. Fetch an item by id for its
stats— the list says so in anotefield rather than leaving you to wonder whether the item has none.
Listing
GET /api/content/<type> takes ?page, ?limit (capped at 20 by default) and
— for asset, mod, server and article — ?appId and ?official.
Ordering is newest-first by id.
There is deliberately no search, no tag filter and no mine: the anonymous
surface exists so an integrator can see the shape of the API, not so it can be
used as a search engine.
?official=1 narrows a listing to the site’s own posts and ?official=0 to
everything else. It exists because for articles that filter is the blog —
the flag is what puts a post there — and without it a caller wanting the blog
has to page the whole article table and filter client-side, getting a different
and shrinking answer every time a member publishes:
GET /api/content/article?official=1&limit=20
Anything other than 0, false or no reads as true, and the parameter is
ignored for types that have no such flag.
Quotas
Per source address, in two windows, both of which must have budget. See Rate limits for the numbers, the headers, and what happens when the counter itself is unavailable.
CORS
access-control-allow-origin: *, and OPTIONS is answered. The surface carries
no credential and sets no cookie, so a page on another site calling it from a
browser is the intended use.