Guides

Packaging & publishing your game

AdminUpdated Sep 22, 2026

Packaging & publishing your game

This guide covers everything between "my game runs locally" and "players can find it": how to package a build, how to create a game listing, the three ways to upload (website, CLI and REST), what moderation checks, what every status means, and how to ship updates, preview builds, roll back and take a game down.

For exact request and response shapes, see Games & uploads API reference. For a five-minute first publish, start with Quickstart.


The lifecycle at a glance

create game (draft)
   │
   ├─ add a cover image  ← required before you can submit a build
   │
   ├─ upload init  →  PUT the .zip  →  upload complete
   │                                        │
   │                          bundle checks (instant, synchronous)
   │                           ├─ fail  → version "failed", game stays as it was
   │                           └─ pass  → game "scanning"
   │                                        │
   │                          automated moderation (typically a minute or two)
   │                           ├─ auto-approve → "published"
   │                           ├─ auto-reject  → "rejected"
   │                           └─ needs review → "pending_review" → a human decides
   │
   └─ later: new version → same checks again (the live version keeps serving) · rollback · unpublish

Each upload creates a new version of the game (v1, v2, v3…). The game has one current version, which is the one players get. Moderation looks at each version separately.


1. Package your build

Supported runtimes

Ask the API which runtimes are accepting uploads right now:

curl https://api.coolgptgames.com/v1/runtimes

Currently available:

runtime

What to upload

Published upload limit

html5

A .zip of any browser game: plain HTML/JS, Phaser, PixiJS, Three.js, Construct, Godot/Unity web exports (see caveats below)

100 MB

pico8

A .zip of PICO-8's EXPORT game.html web build

8 MB

twine

A .zip of Twine's Publish to File HTML

10 MB

bitsy

A .zip of Bitsy's downloaded game.html (the export that includes the engine)

8 MB

The maxUploadBytes value returned by GET /v1/runtimes is the limit, and upload complete enforces it for each runtime: a bigger zip fails with upload_too_large.

Everything is uploaded as a .zip. PICO-8, Twine and Bitsy builds are also checked for an engine signature (for example, a Twine upload must actually contain Twine story data). If the check fails you get invalid_cart_format with a hint for re-exporting from that tool.

Zip layout and the entry file

  • Your zip must contain an index.html (or index.htm). The file name is case-insensitive.

  • If there is more than one, the one closest to the top of the archive is used. A single top-level folder is fine:

my-game.zip
├── index.html        ← entry (preferred: at the root)
├── game.js
├── style.css
└── assets/
    ├── sprites.png
    └── music.ogg
my-game.zip
└── dist/
    ├── index.html    ← also fine: the shallowest index.html wins
    └── assets/…
  • Use relative paths for everything your game loads (assets/sprites.png or ./assets/sprites.png). Your game is served from its own folder on our game CDN, so a root-absolute path such as /assets/sprites.png points at the CDN root rather than your build, and it will 404.

  • Your game can't load scripts, images, fonts or media from third-party hosts. Put every asset inside the zip (see Calling external hosts for the one exception).

Allowed file types

Every file in the archive must have one of these extensions (case-insensitive):

Kind

Extensions

Markup, code & data

.html .htm .js .mjs .css .json .map .txt .wasm

Images

.png .jpg .jpeg .gif .webp .svg .ico

Audio

.mp3 .ogg .wav

Fonts

.woff .woff2 .ttf

3D

.glb .gltf .bin

Engine data

.pck (Godot packs), .data (Emscripten / Unity preload data)

Pre-compressed files

.br, .gz (for example Build.wasm.br, game.framework.js.gz)

OS clutter is skipped silently, so you don't have to strip it: __MACOSX/ folders, .DS_Store, Thumbs.db and desktop.ini are simply left out of the stored build.

Any other extension rejects the whole upload with disallowed_extension, and the error names the file. Common culprits:

File

Why it's there

Fix

README.md, LICENSE (no extension)

Project files

Leave them out of the build folder

*.mp4, *.webm, *.m4a

Video, AAC audio

Remove, or convert (for example to .ogg)

Engine exports (Godot, Unity, Emscripten): web exports work as-is, including compressed builds. A file named x.<type>.br or x.<type>.gz is served as the inner type (so Build.wasm.br is served as WebAssembly) with Content-Encoding: br or gzip, which is what Unity's loader and WebAssembly.instantiateStreaming expect. Compressed code is decompressed and scanned by moderation like any other code. A bare archive.gz with no inner extension is served as an opaque gzip file.

Structural limits

Limit

Value

Error code

Zip size

See the runtime table above

upload_too_large

Total size after unzipping

300 MB

uncompressed_too_large

Number of files

2,000

too_many_files

Compression ratio of any single file

100 : 1

compression_ratio_exceeded

Time to read the archive

20 seconds

extraction_timeout

Empty archive

Not allowed

empty_upload

Folders don't count toward the file limit. The compression-ratio limit protects against zip bombs. A normal asset never gets close to it, but a large file of zeros or a huge repetitive text file might.

Path rules

These are rejected for safety:

Problem

Example

Error code

Parent-directory segments

../secrets.txt

path_traversal

Backslashes in names

assets\sprite.png

path_traversal

Absolute paths / drive letters

/etc/passwd, C:\game\index.html

absolute_path

Symbolic links

a symlinked assets/ folder

symlink

Not a zip at all

a renamed .rar, a truncated download

invalid_archive

Archives made on Windows with some older tools store backslashes. Re-zip with a modern tool or from the command line.

Making a clean zip

# macOS / Linux: zip the CONTENTS of your build folder
cd dist
zip -r ../my-game.zip .

OS clutter is skipped for you, so no exclusions are needed. Leave out source maps (-x "*.map") if you'd rather not publish your original source.

# Windows: use PowerShell 7+ (older Windows PowerShell 5.1 can write backslashes into zip paths)
Compress-Archive -Path dist\* -DestinationPath my-game.zip

Before you upload, list the archive and check it against the tables above:

unzip -l my-game.zip

Design for the automated play-test

Moderation loads your game in a headless browser, sends some basic input (arrow keys, Space, W/A/S/D and a click near the middle of the screen) and takes screenshots over the first 30 seconds. To pass cleanly:

  • Draw something immediately. A blank screen after warm-up is an automatic rejection (blank_screen).

  • A title screen is fine. Make sure a click or key press starts the game, and that something visible is on screen while it loads.

  • Don't require sign-in, an external account or a network connection before anything is drawn.


2. Create the game listing

A game starts as a draft. Drafts are visible only to you.

Fields

Field

Required

Limit

Notes

title

Yes

1 – 120 chars

Also generates the URL slug when the game is created (e.g. space-dodger-3f9a1c). The slug does not change if you rename the game later.

description

No

4,000 chars

Short description shown on the game page.

instructions

No

4,000 chars

"How to play".

categorySlug

No

One of: action, puzzle, arcade, racing, shooter, strategy, sports, io. An unknown slug is silently ignored (the game ends up with no category), so double-check the spelling.

tagline

No

160 chars

One-line pitch.

longDescription

No

8,000 chars

A longer store-page write-up. Helps search ranking.

faq

No

Up to 20 entries

Each entry is { question, answer }: question 1–300 chars, answer 1–2,000 chars. Published as FAQ structured data for search engines.

websites

No

Up to 5 URLs

Your own sites (store page, socials, homepage). Each must start with http:// or https:// and be at most 500 chars. Duplicates and blanks are removed. Links are rendered rel="nofollow ugc".

aiGenerated

No

boolean

Shows an "AI-made" label on the game page. Please be honest here.

aiDisclosure

No

2,000 chars

Optional note on which tools or models you used.

embeddable

No (edit only)

boolean

Default true. Turn it off to stop other sites embedding your game. See Embedding & the score bridge.

websiteUrl (a single URL) is still accepted for older clients and is treated as a one-item websites list. Prefer websites.

Tags. Games can be grouped by tags for "related games", but there is currently no creator-facing way to set tags on your game.

Human verification

On the website, creating a game includes a human-verification check (Cloudflare Turnstile). API clients may omit turnstileToken. If you do send one and it's invalid, the request fails with captcha_failed.

Retry-safe creation

Send an Idempotency-Key header (any unique string, up to 128 characters) when you create a game. If the request is retried with the same key, you get the same game back (200 with "idempotent": true) instead of a duplicate draft.

curl -X POST https://api.coolgptgames.com/v1/games \
  -H "Authorization: Bearer $CGG_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: space-dodger-first-publish" \
  -d '{
    "title": "Space Dodger",
    "description": "Dodge asteroids. Grab stars. Don't blink.",
    "instructions": "Arrow keys to move, Space to boost.",
    "categorySlug": "arcade",
    "tagline": "A 60-second arcade dodger",
    "websites": ["https://example.itch.io/space-dodger"],
    "aiGenerated": true,
    "aiDisclosure": "Code drafted with an AI assistant; art hand-drawn."
  }'
{ "game": { "id": "g_…", "slug": "space-dodger-3f9a1c", "status": "draft" } }

Keep the id. Every other creator endpoint uses it.

Editing metadata later

POST /v1/games/{id} takes the same fields (all optional) and updates only the fields you send. Metadata edits take effect immediately and do not trigger re-moderation. They don't create a new version either.

  • To clear your websites, send "websites": [].

  • Sending an empty string ("") for a text field such as description clears it.


3. Cover, screenshots & loading screen

Images are uploaded as raw bytes to POST /v1/games/{id}/media?kind=…, with the image's content type as the Content-Type header.

kind

What it is

Count

cover

The card image in the catalog and on your game page. Required before you can submit a build.

1 (uploading again replaces it)

screenshot

Gallery images above your description (this is the default if kind is omitted)

Up to 8

loading

Shown while your game boots. If unset, the cover is used instead.

1 (uploading again replaces it)

Requirements for every image:

  • Formats: PNG, JPEG, GIF or WebP. The format is detected from the file's contents, not its name or header. Anything else is rejected with bad_image.

  • Size: at most 5 MB per image (too_large).

  • Dimensions: none are enforced. Catalog cards display covers at 4:3, so a 4:3 cover (for example 1200 × 900) looks best.

# Cover (required)
curl -X POST "https://api.coolgptgames.com/v1/games/$GAME_ID/media?kind=cover" \
  -H "Authorization: Bearer $CGG_API_KEY" \
  -H "Content-Type: image/png" \
  --data-binary @cover.png

# A screenshot
curl -X POST "https://api.coolgptgames.com/v1/games/$GAME_ID/media?kind=screenshot" \
  -H "Authorization: Bearer $CGG_API_KEY" \
  -H "Content-Type: image/jpeg" \
  --data-binary @shot1.jpg

Each upload returns the image's key and public url. Screenshot uploads also return the full gallery in order. Keep the key: you need it to remove an image or reorder the gallery:

  • DELETE /v1/games/{id}/media with { "key": "…" } removes a cover, loading screen or screenshot.

  • POST /v1/games/{id}/media/reorder with { "keys": ["…", "…"] } sets the gallery order.

If you lost a key, it's the part of the image URL after the CDN host (it starts with media/).

Image uploads are rate-limited to 60 per hour per IP address.


4. Upload a build

Uploading always takes three steps. Every client (website, CLI, SDK) does the same thing behind the scenes:

  1. Init. POST /v1/upload/init reserves a new version number and returns a one-time uploadUrl plus an uploadId.

  2. PUT. Send the raw zip bytes to uploadUrl, with Content-Type: application/zip. The URL goes directly to storage, expires after 15 minutes, and takes no Authorization header.

  3. Complete. POST /v1/upload/complete with the uploadId. The API unpacks and validates the bundle right away and returns either an error code from the packaging tables above or { "status": "scanning" }, which means automated moderation has started.

Via REST (curl)

API=https://api.coolgptgames.com
AUTH="Authorization: Bearer $CGG_API_KEY"

# 1. init
INIT=$(curl -s -X POST $API/v1/upload/init -H "$AUTH" -H "Content-Type: application/json" \
  -d "{\"gameId\":\"$GAME_ID\",\"runtime\":\"html5\",\"filename\":\"my-game.zip\"}")
UPLOAD_URL=$(echo "$INIT" | jq -r .uploadUrl)
UPLOAD_ID=$(echo "$INIT" | jq -r .uploadId)

# 2. PUT the bytes (Content-Type must be exactly application/zip)
curl -X PUT "$UPLOAD_URL" -H "Content-Type: application/zip" --data-binary @my-game.zip

# 3. complete
curl -X POST $API/v1/upload/complete -H "$AUTH" -H "Content-Type: application/json" \
  -d "{\"uploadId\":\"$UPLOAD_ID\"}"
# → {"gameVersionId":"…","status":"scanning"}

Via REST (JavaScript)

const API = "https://api.coolgptgames.com";
const headers = {
  Authorization: `Bearer ${process.env.CGG_API_KEY}`,
  "Content-Type": "application/json",
};

async function call(method, path, body) {
  const res = await fetch(API + path, { method, headers, body: body && JSON.stringify(body) });
  const json = await res.json();
  if (!res.ok) throw new Error(`${json.error.code}: ${json.error.message}`);
  return json;
}

// zipBytes: a Buffer / Uint8Array / Blob of your .zip
export async function uploadBuild(gameId, zipBytes) {
  const { uploadUrl, uploadId } = await call("POST", "/v1/upload/init", {
    gameId,
    runtime: "html5",
    filename: "my-game.zip",
  });

  const put = await fetch(uploadUrl, {
    method: "PUT",
    headers: { "Content-Type": "application/zip" },
    body: zipBytes,
  });
  if (!put.ok) throw new Error(`storage PUT failed: HTTP ${put.status}`);

  return call("POST", "/v1/upload/complete", { uploadId }); // { gameVersionId, status: "scanning" }
}

Optional fields on init:

Field

Purpose

See

realtime: true

Turns on multiplayer rooms for this version

Multiplayer

connectHosts: [...]

External HTTPS hosts this version may call

below

gamification: {...}

Declares achievements and XP intent for this version

XP, achievements & social

Via the website

Upload a game on coolgptgames.com runs the whole flow for you: pick your .zip, fill in the listing, add a cover (required) and up to 8 screenshots, then press Publish game. The page waits a few seconds for the verdict. If moderation is still running, your dashboard updates when it finishes.

Via the CLI

arcadey publish <dir> --cover <image> zips a folder, creates the game, uploads its cover (and any screenshots) and the build, and waits for the verdict. The cover is required for a new game, and the CLI stops before making any API call if it's missing, so you never get a stray draft. It prints the new game's id and the command for shipping updates.

To ship an update to an existing game, add --game <id> (or put "id" in game.json). --cover is optional then and replaces the current cover. --changelog adds release notes.

An optional game.json in the folder can set title, category, description, instructions, realtime, connectHosts, gamification (declared achievements), cover, screenshots and id. See CLI for install, login, every flag and the exit codes.

Via the Publisher SDK

publish({ title, files, cover, … }) creates a game and publishes it with its cover in one call, and publishVersion(gameId, { files, … }) ships an update to an existing game (optionally replacing the cover or adding screenshots). Both wait for the verdict. gamification can be passed on either. Lower-level helpers (uploadCover, uploadScreenshot, uploadVersion) are there if you want each step yourself. See Publisher SDK.

The CLI and the Publisher SDK don't yet send a changelog and a gamification block in the same upload, and refuse the combination. The REST endpoint POST /v1/games/{id}/versions accepts both.

Upload rate limits

Starting uploads (upload/init and games/{id}/versions, which share one allowance) is limited per account, per hour, and the allowance grows with your creator level:

Creator level

Uploads per hour

1 – 2

5

3 – 5

8

6 – 9

12

10 – 14

20

15 – 24

30

25+

50

A failed upload still used up its slot. So validate your zip locally before retrying in a loop. All API calls are also subject to the general per-IP limit (see Errors & limits).

When complete fails

If complete returns an error, that version is marked failed and your game's status doesn't change (a live game stays live). Fix the problem and start again from init. Retrying complete itself after it succeeded is harmless: it returns 200 with "idempotent": true and the version's current state. Each attempt uses a new version number, so it's normal to see gaps like v1, v4.

Error code

Meaning

cover_required

Upload a cover first (kind=cover), then retry. Only an image uploaded through the media endpoint counts as a cover.

no_upload

The PUT never happened, failed, or went to an expired URL.

game_removed (409)

The game was delisted or removed after a DMCA notice, and can't take new builds. upload/init and games/{id}/versions refuse it too.

Any code from the packaging tables

The bundle itself is invalid. The error message names the file or limit.


5. Moderation

Every version goes through the same pipeline. It typically finishes within a minute or two of complete.

What gets checked

Check

What it looks for

Bundle validation (at complete)

Everything in section 1: format, entry file, file types, sizes, paths.

Static code scan

Parses every .js / .mjs file (compressed .br / .gz code is decompressed first), inline <script> and on…= HTML attribute. Looks for network calls to hosts that aren't allowed, dynamic code execution, cookie or parent-frame access, forced navigation, obfuscation and known crypto-miners. Hosts you declared in connectHosts count as allowed.

Play-test render

Loads the game in a headless browser, plays briefly and captures screenshots. Checks that the game draws something, and flags any network request to a host you didn't declare.

The three outcomes

Verdict

Happens when

Game not live yet: it becomes

Update to a live game

Auto-approve

Every check is clean and confidence is high

published right away, fully discoverable

The new version goes live in place of the old one

Auto-reject

Any hard-fail signal (table below)

rejected

Only the new version is failed. The game stays live on its current version.

Needs review

Anything that needs a human's judgement

pending_review until a moderator decides

The new version waits (passed). The game stays live on its current version.

Hard-fail signals (automatic rejection):

Signal

Reason code

Code or HTML loads from, or sends data to, a host that isn't allowed and isn't declared in connectHosts (hard-coded URL in fetch, WebSocket, EventSource, sendBeacon, XMLHttpRequest.open, import(), an .src = assignment, or a <script>/<img>/<iframe>/<audio>/<video>/<link> tag)

data_exfiltration

A network request to an undeclared host during the play-test

data_exfiltration

Known crypto-miner code

cryptominer

Blank screen, or the game isn't playable

blank_screen

Doesn't look like a game at all

not_a_game

Byte-identical to a bundle another creator already uploaded

exact_duplicate

An exploitable XP or achievement economy

economy_abuse

Review signals (sent to a human, not rejected). None of these is forbidden. They just need a human to look:

  • eval(…), new Function(…), setTimeout("code string"), any .constructor(…) call

  • new XMLHttpRequest(), or fetch/WebSocket with a URL built at runtime

  • dynamic import(), service-worker registration

  • window.top / window.parent, document.cookie, document.write

  • changing location, window.open(…), javascript: URLs, <meta http-equiv="refresh"> to another host

  • inline on…="…" event-handler attributes in HTML

  • .wasm files (they can't be statically analysed)

  • heavily obfuscated code (lots of _0x… identifiers), or JS/HTML that fails to parse

  • possible sexual content, graphic violence, hate symbols or IP infringement

  • title or description text that reads as spam

  • a near-duplicate of another game

  • low AI confidence in any of the above

Tip: a clean, un-minified or lightly minified build without eval, inline onclick="" handlers or runtime-built URLs is much more likely to be auto-approved. Obfuscators almost always send you to human review.

Re-uploading the same zip: your own uploads never count as duplicates, so re-uploading an unchanged zip (for example after a scan hit an error) works.

Reading the result

GET /v1/me/games/{id} (the status page in your dashboard, or arcadey status <gameId>) returns every version with its moderation data:

{
  "game": { "id": "g_…", "status": "rejected", "visibility": "full", "currentVersionId": null, "…": "…" },
  "versions": [
    {
      "id": "v_…",
      "versionNumber": 2,
      "status": "failed",
      "isCurrent": false,
      "moderation": { "verdict": "needs_review", "confidence": 0.62, "status": "complete", "error": null },
      "review": {
        "decision": "reject",
        "reasonCodes": ["ip_infringement"],
        "notes": "Sprites are lifted from a commercial game.",
        "at": "2026-09-20T14:02:11.000Z"
      }
    }
  ]
}
  • moderation is the automated result: verdict (auto_approve / auto_reject / needs_review), confidence (0 to 1), and job status (queued / running / complete / error).

  • review is the decision with its reasonCodes and notes. It's filled in for every approval and rejection, whether a human made it or the automated pipeline did. Automated decisions have notes of "Approved by automated moderation." or "Rejected by automated moderation.", and their reason codes come from the hard-fail signals above. A version held for a human has no review until the reviewer decides.

  • Every decision also sends a game.approved / game.rejected webhook if you've set one up. The payload includes the gameVersionId and automated: true or false (see Webhooks). A human decision also emails you.

Reason codes

Code

Meaning

nsfw_sexual, nsfw_nudity

Sexual content or nudity

graphic_violence, gore

Graphic violence or gore

hate_symbols, harassment

Hateful imagery or harassment

ip_infringement, trademark

Someone else's characters, art, music or brand

broken_not_playable, blank_screen

Doesn't load, or nothing is drawn

not_a_game

Not interactive, or not a game

low_effort_duplicate, exact_duplicate

A copy or near-copy of an existing game

malicious_code

Risky code patterns (eval, obfuscation, cookie or frame access…)

cryptominer

Crypto-mining code

data_exfiltration

Talks to hosts it isn't allowed to

phishing_ui

Fake login or payment screens

misleading_metadata

The title, description or cover doesn't match the game

spam, contact_info_spam

Spammy text, or ads for contact details

gambling

Real-money gambling mechanics

age_inappropriate

Unsuitable for the general audience

economy_abuse

Farms XP, achievements or scores

other

See the reviewer's notes

After a rejection

There is no appeal button. Fix the issue and upload a new version. It goes through moderation again from scratch. If you think the rejection was a false positive (for example a legitimate eval in a scripting engine), removing or isolating the flagged pattern is the most reliable fix.


6. Status reference

Game status

status

Meaning

Playable by the public?

draft

Created, but no build has passed validation yet

No

scanning

A build passed validation and automated moderation is running

No (see the note below)

pending_review

Automated checks want a human to look

No

published

Live

Yes

rejected

The game's latest build failed moderation before the game ever went live (a rejected update to a live game doesn't change its status)

No

delisted

You unpublished it, or a moderator took it down

No

dmca_removed

Removed after a valid copyright notice

No

visibility is either full (in the catalog, search and recommendations) or limited (playable by direct link, but hidden from discovery). Approved games are full. A moderator may set a game to limited while looking into player reports.

Updating a live game: your game stays published and keeps serving its current version while the update is moderated. When the update is approved, it goes live in place of the old version (visibility and the original publish date are kept). If it's rejected or held for human review, only the new version is affected: players keep getting the current version. So scanning, pending_review and rejected only describe a game that hasn't gone live yet. To follow an update, watch the new version's status in GET /v1/me/games/{id}.

Version status

status

Meaning

uploaded

Reserved by init, or bytes received but not yet processed

scanning

Automated moderation is running

passed

Cleared the automated checks and is waiting for a human reviewer. (A version approved after its game was taken down also stays passed.)

failed

Failed bundle validation, automated moderation or human review (or moderation hit an internal error)

live

Approved. It is (or was) served to players. isCurrent: true marks the one being served now.


7. Updating your game

To ship a new build, upload a new version of the same game. Use either:

  • POST /v1/upload/init with the existing gameId, or

  • POST /v1/games/{id}/versions, which takes the same fields plus a changelog (up to 2,000 chars) shown to players as "What's new".

Both return an uploadUrl / uploadId. Then PUT and complete as usual. Both count against the same hourly upload allowance. The CLI (arcadey publish <dir> --game <id>) and the Publisher SDK (publishVersion(gameId, …)) do all of this for you.

curl -X POST https://api.coolgptgames.com/v1/games/$GAME_ID/versions \
  -H "Authorization: Bearer $CGG_API_KEY" -H "Content-Type: application/json" \
  -d '{"runtime":"html5","changelog":"Boss fight rebalanced; new level 5."}'
# → 201 { "uploadUrl": "…", "uploadId": "…", "expiresAt": 1790000000000 }

Each version gets its own moderation pass, and the game stays live on its current version until the update is approved. Your listing, cover, screenshots, ratings, play counts and leaderboards stay with the game across versions.

Achievements across versions. Both endpoints accept a gamification block (declared achievements). Leave it out and the new version keeps the game's current achievements: the economy audit knows about them, and your existing unlockAchievement calls keep working. Send a block and, on approval, it replaces the game's set: achievements missing from the new block are deactivated (an empty list deactivates them all). A changelog can only be sent on games/{id}/versions.

Changing your game without a new build: remote config values, store items and metadata can all be changed without re-uploading or re-moderation. See Remote config & analytics and In-game economy.


8. Preview builds & instant rollback

Private preview

GET /v1/me/games/{id}/preview returns a play link that only you can generate, for a build that isn't public, such as a draft, a version in review, or a rejected build you want to reproduce.

  • With no parameters, it previews the current version (or, if nothing is live yet, the newest version).

  • Add ?versionId=… to preview a specific version.

  • The link has a short-lived access ticket and expires about 15 minutes after it's issued. Request a new one when it expires.

  • A version that failed at complete has no build to preview (no_build).

curl "https://api.coolgptgames.com/v1/me/games/$GAME_ID/preview?versionId=$VERSION_ID" \
  -H "Authorization: Bearer $CGG_API_KEY"
# → { "playUrl": "https://…?t=…", "version": { "id": "…", "versionNumber": 3, "status": "passed" } }

The dashboard's Preview button does the same.

Rollback

POST /v1/me/games/{id}/versions/{versionId}/rollback makes an earlier version current again right away, with no re-moderation. The game becomes published with full visibility.

  • You can only roll back to a version that was approved and published before (status live). A version that's still waiting for a human (passed), or one that failed, returns 400 not_approved.

  • A game that was delisted or removed after a DMCA notice can't be brought back by rolling back: that returns 409 game_removed. A deleted game returns 404.

  • Use it to undo a bad update that was approved.

await fetch(
  `https://api.coolgptgames.com/v1/me/games/${gameId}/versions/${versionId}/rollback`,
  { method: "POST", headers: { Authorization: `Bearer ${process.env.CGG_API_KEY}` } },
).then((r) => r.json());
// → { ok: true, currentVersionId: "…", versionNumber: 2 }

9. Unpublishing

DELETE /v1/games/{id} (the Unpublish button in the dashboard) removes the game from the catalog and from your games list:

  • Its status becomes delisted and the game page stops loading for everyone, including you.

  • It works on drafts as well, to get rid of an abandoned draft.

  • There's no self-serve undo. Once unpublished, the game no longer appears in GET /v1/me/games and its manage view returns 404. If you want a game back online later, don't delete it. Just stop promoting it, or roll back to a version you're happy with.

Once a game is unpublished, every write to its id (metadata edits, new uploads and versions, preview, rollback and images) returns 404.

A moderator can also delist a game (for example after player reports). That shows up as status delisted in your dashboard, while the game still appears in your list. A delisted game can't take new builds or be rolled back (409 game_removed).


10. Copyright (DMCA) from a creator's point of view

  • Anyone can file a copyright takedown notice against a game through the DMCA form on coolgptgames.com. Filing a notice doesn't remove anything by itself. A person reviews it first.

  • If the notice is valid, the game's status becomes dmca_removed and it stops being playable. New uploads and rollbacks for it are refused (409 game_removed), so a removed game can't be republished from your side.

  • If you believe the removal is a mistake (you own the work, have a license, or it's fair use), you can send a counter-notice. Under the DMCA, removed content may be restored if the claimant doesn't take legal action within the statutory window. See the DMCA & Copyright policy on coolgptgames.com. - Repeat infringers are terminated: three substantiated strikes close the account.

  • Don't re-upload removed content under a new game. Duplicate detection and human review both look for it, and it counts toward the strike policy.

To avoid problems in the first place: use only assets you made, licensed or that are clearly public domain / CC0. Don't use other games' characters, logos or music. Credit licensed assets in your description.


Calling external hosts (connectHosts)

Games run in a locked-down sandbox and can only reach our own services by default. If your game needs a public HTTPS API (say, a word list or an emoji service), declare the host on upload/init or games/{id}/versions:

{ "gameId": "g_…", "runtime": "html5", "filename": "my-game.zip",
  "connectHosts": ["https://api.example.com"] }

Rules (a violation fails init with bad_connect_hosts):

  • Exact HTTPS hosts only. A bare host such as api.example.com is treated as https://api.example.com. Paths are ignored, since the declaration is per host.

  • No wildcards, IP addresses, localhost, single-word hosts, or internal names (.local, .internal, .lan, .corp…).

  • No coolgptgames.com origins. They're already allowed.

  • At most 10 hosts per version (too_many_connect_hosts).

  • The API you call must send Access-Control-Allow-Origin: * and must not need cookies or credentials. Your game's origin is opaque (null).

  • Declared hosts are checked by moderation on every version. Declare them again each time you upload.

  • Moderation treats declared hosts as allowed: a literal fetch("https://api.example.com/…") to a declared host passes the code scan and the play-test. Requests to any undeclared host are still flagged.

How the browser enforces this is covered in How games run. If the data is static, just put it in your zip as a .json file: no network, no CORS, nothing to review.


Troubleshooting checklist

Symptom

Likely cause

cover_required on complete

Upload kind=cover first.

missing_entry

No index.html in the zip. Check you zipped the build output, not the project.

disallowed_extension

A file type that isn't on the allowlist, such as README.md, a file with no extension, or video. The message names it.

invalid_archive

Not a real zip, or a truncated upload. Re-zip and upload again.

no_upload

The PUT was skipped, failed, or the 15-minute URL expired. Start again from init.

Storage PUT returns 403

Content-Type wasn't exactly application/zip, or the URL expired.

rate_limited on init or versions

Hourly upload limit reached. Wait, and validate locally before retrying.

game_removed (409)

The game was delisted or DMCA-removed. It can't take new builds or be rolled back.

Instantly rejected with exact_duplicate

Your zip is byte-identical to another creator's upload.

Stuck in pending_review

A human reviewer will decide. Check the review signals above to avoid it next time.

Update doesn't show up for players

It's still in moderation (the old version keeps serving until it's approved), or it was rejected. Check the new version's status in GET /v1/me/games/{id}.

Assets 404 in the preview

You're using root-absolute paths (/assets/…). Switch to relative paths.

Was this page helpful?