Skip to content
ShortPlay Developer Docs

Zip format, versions, and rollback

What your zip needs to contain

The upload flow, endpoint by endpoint

  1. POST /v1/uploads/init — start an upload for a game (title + hook + tag); the hook is max 50 characters (counted as visible characters), and a longer one is refused here with 400 bad_request, before it counts against your limit (api/src/upload-routes.ts TAGLINE_MAX, api/src/taglineLimit.ts; /complete re-checks it for a new game); returns an uploadId and a uploadUrl to PUT your zip to. Rate-limited to 10 uploads per account per hour (api/src/upload.ts:64, api/src/upload-routes.ts:268-329).
  2. PUT /v1/uploads/:id/bundle — stream your zip body to that URL (api/src/upload-routes.ts:335-367).
  3. POST /v1/uploads/:id/complete — send your confirmed manifest (+ title/hook/tag); the bundle is validated (zip structure, index.html presence, size) and a draft version is staged. Nothing is published yet (api/src/upload-routes.ts:374-).
    • Without gameId (your first upload): creates the game and its v1 draft version, so you can preview it right away (GET /v1/games/:id/versions/1/preview, or create a preview link). The game row is written from title, hook, tag, genre, tags, description, accent, orientation and confirmedManifest.
    • With gameId (a new version on an existing game): stages the next draft version (the next version number after the highest existing one) with its releaseNotes and your confirmedManifest, and points the game row's bundle_version/bundle_path at it. It never writes the game's title, hook, tag, genre, description or accent (edit those separately with the game-details endpoint), and it does not change what players get: the confirmed manifest rides on the new version and is applied to the game when that version is published. (If the game has never been live, the manifest is applied to the game row immediately, since nothing in the feed can change.) On a game that is already live it also leaves updatedAt alone, so a card nothing changed on doesn't look freshly updated. A retried /complete for the same upload is idempotent (it returns the same version, not a duplicate). (api/src/upload-routes.ts existing-game branch; api/src/upload.ts stageExistingGameUpload.)
  4. POST /v1/games/:id/publish — promotes a draft (or a specific version, via { "version": N } in the body) to live. This is the only step that makes a game appear in the feed, and only after the next pool-rebuild cron. Publish also enforces a completeness gate: a game with no hook or no genre tag set in Details is refused with 422 details_required — the feed never shows an incomplete listing (api/src/upload-routes.ts:612-786). The response includes url, the public game link (https://shortplay.io/game/<id>). Omit version, or send null, to publish the newest draft; otherwise a positive integer (a numeric string such as "1" is also accepted) — anything else is a 400 (error.reason: "invalid_version"); an unknown or other-game version is a 404 (version_not_found) — neither publishes anything.

Versions

Every upload after the first becomes a new version on the game's ladder — versions are immutable once promoted, and only one version is ever live at a time. You can:

Want to play a build on your phone, inside the real ShortPlay frame, before (or without) publishing it? Mint a preview link for that build. It is a private, unlisted URL: the game never appears in the feed because of it.

Release notes

Each version can carry release notes (max 2000 characters, api/src/upload-routes.ts:1586 MAX_RELEASE_NOTES_LEN), attached at completion time and surfaced back in the version-history response. They're free text — a changelog line for players or your own record, not machine-parsed.