Integrate Zyplo
- You upload one HTML file. We put it in the feed.
- We don't charge you, we don't pay you, we don't lock you in.
- Your creator profile links out to your itch, your Steam, your Patreon.
Who this is not for
If you export multi-file WebGL from Godot, Unity or GameMaker, Zyplo will not host it. The format is one self-contained HTML file — a .wasm plus a .pck cannot be uploaded, today or on a roadmap. Better you know now than after a week of trying.
Publishing a game
Zyplo speaks MCP. Connect it once and your assistant can create a game, validate it, upload it as a draft, edit it and publish it — without you leaving the chat.
- Claude — In Claude, add a custom connector with this URL. Sign-in happens in the browser; no key to copy or store.
https://zyplo.io/api/mcp - ChatGPT — Same idea, different endpoint. Enable Developer Mode in ChatGPT, then add the connector.
https://zyplo.io/api/chatgpt/mcp - Cursor, Codex and other MCP clients — Drop this into your client's MCP config. Any client that speaks streamable HTTP and OAuth will work.
Auth is OAuth with dynamic client registration and PKCE. Legacy zyplo_sk_ keys still work if you already have one.
Over HTTP
One POST publishes a game. Nothing else is required — no SDK, no build step, no script tag inside your game.
curl -X POST https://zyplo.io/api/upload-game \
-H "Authorization: Bearer $ZYPLO_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Tap the Comet","description":"One tap. Do not miss.","html_content":"<!doctype html>...","tags":["arcade"],"published":false}'
ZYPLO_KEY is a personal key (zyplo_sk_…) or an OAuth access token (zyplo_at_…). Upload as a draft first (published: false) and look at it before it goes live. openapi.json
Check before you upload
Add ?dry_run=true and the same call validates without creating anything. It reports every problem at once instead of stopping at the first, and writes nothing — no file, no game, no rate-limit stamp — so you can iterate as fast as you like.
curl -X POST "https://zyplo.io/api/upload-game?dry_run=true" \
-H "Authorization: Bearer $ZYPLO_KEY" \
-H "Content-Type: application/json" \
-d @game.json
Answers 200 whether or not the game is valid: the status says the check ran, ok says the verdict. A bad token still answers 401. It does not check the rate limit or the cover image, so ok: true means the game is valid, not that the upload is guaranteed to succeed.
The format
These are the real rules, read straight out of the same module the uploader and the MCP prompt use. If this page and the API ever disagree, that's a bug — tell us.
Hard rules — the upload rejects violations
- Single self-contained file. No external scripts, no external CSS, no
<link rel="stylesheet">, nofetch/XMLHttpRequest/WebSocket/EventSource, noeval/new Function, no cookies, nowindow.open, no<iframe>, no<form>, no<meta http-equiv="refresh">. - Asset URLs must be either
data:,blob:, or absolutehttps://. No relative paths. - HTML must not exceed 5 MB. Aim for under 90,000 characters so the game also works through ChatGPT.
- Total document starts with
<!doctype html>or<html.
Design rules — not enforced, but ignore them and the feed buries you
- Viewport:
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, viewport-fit=cover">. - Locked vertical 9:16 layout. Fill the screen. No scroll.
- Controls are tap/swipe only — no keyboard, no mouse hover. Use
pointerdown/touchstart. - Loop length: a complete attempt lasts 10–30 seconds. Game restarts with one tap.
- Show a one-line title screen, an obvious tap-to-start, a visible score, and a game-over screen with a restart button.
- Vibrant, high-contrast visuals. Use bold colors, CSS, and SVG/canvas. No external images, no fonts.
- Performance: should run smoothly on a 4-year-old phone. Avoid heavy physics or huge canvases.
5 MB— max HTML size90k— characters — stay under this to also work through ChatGPT10–30s— one full attempt10s— between uploads
A game that passes
A minimal game that satisfies every rule above. Play it, then read it — it is short on purpose. Play the example
It is a single file, so your browser's View Source shows you the whole game.
Why an upload gets rejected
Every failure answers with a stable code, the field at fault, and what to do about it. Branch on code, never on the message text — messages can be reworded, codes will not.
{
"error": "Zyplo acepta HTML autocontenido: ...",
"code": "external_script",
"field": "html_content",
"detail": "https://cdn.example/phaser.js",
"fix": "Inline the library into a <script> block.",
"docs_url": "https://zyplo.io/en/integrate#error-external_script"
}
The error string is unchanged from before codes existed, so nothing that reads it breaks. code is internal_error when the failure is one we have not described yet — that is our bug, and we want to hear about it.
not_html400- The payload does not start like an HTML document. Field:
html_contentFix: Send the whole document, starting with<!doctype html>. Not a fragment, not JSON, not a base64 blob. external_script400- A
<script src="...">points somewhere else. Field:html_contentFix: Inline the library into a<script>block. If it is too big to inline, the game does not fit Zyplo. relative_asset400- An asset uses a relative path, which has nothing to resolve against once the file is served on its own. Field:
html_contentFix: Embed the asset as adata:URI, or point at an absolutehttps://URL. forbidden_tag400- The document contains
<iframe>,<frame>,<frameset>,<object>,<embed>,<applet>,<form>or<base>. Field:html_contentFix: Remove it. Games run inside a sandboxed iframe already, so nesting one or posting a form cannot work. meta_refresh400- A
<meta http-equiv="refresh">would navigate the player away from the feed. Field:html_contentFix: Remove it and restart the game in JavaScript instead. dangerous_url400- A URL uses a scheme that is not allowed (
javascript:,vbscript:,file:,ftp:, or plainhttp:). Field:html_contentFix: Usehttps://,data:orblob:. For behaviour on click, attach a listener instead of ajavascript:URL. network_api400- The code calls
fetch,XMLHttpRequest,WebSocket,EventSource,importScripts,eval,new Function,navigator.sendBeacon,document.cookieorwindow.open. Field:html_contentFix: Remove the call. Games are offline by design: keep state in memory or inlocalStorage. There is no in-game backend and no in-game SDK. missing_token401- No Authorization header was sent. Fix: Send
Authorization: Bearer <token>with a zyplo_sk_ key or a zyplo_at_ OAuth token. invalid_token_format401- The Bearer token is neither a zyplo_sk_ key nor a zyplo_at_ OAuth token. Fix: Check you are not sending a Supabase JWT or a raw session token. Zyplo tokens start with zyplo_.
invalid_api_key401- The zyplo_sk_ key does not exist or was revoked. Fix: Issue a new key from Creator Profile → API & Connectors, or connect over OAuth instead.
invalid_oauth_token401- The OAuth access token expired or was revoked. Fix: Refresh the token. If your client cannot, remove and re-add the Zyplo connector.
account_suspended401- The account behind this token is suspended, so it cannot upload or edit games. Fix: Nothing to retry. Get in touch if you think the suspension is a mistake.
game_not_found401- No game with that game_id belongs to this account. It may have been deleted, or the id may belong to someone else. Fix: Call list_my_games (MCP) to get the ids this account actually owns. Do not retry with the same id.
invalid_body400- The request arrived without a JSON object as its body. Field:
bodyFix: Send the fields as a JSON object with Content-Type: application/json. An empty request, a bare string and an array all land here;detailsays which one arrived. missing_field400- A required field was absent or empty. Fix: See
fieldfor which one. Send it with a non-empty value. conflicting_fields400- Two fields were sent that cannot both apply. Fix: Send cover_url or cover_data_url, never both.
unsupported_tag400- A tag is not one of the twenty Zyplo accepts. Field:
tagsFix: Pick from the enum in the OpenAPI spec (arcade, puzzle, skill, casual, humor, meme, snake, breakout, tetris, dodge, memory, logic, reflex, timing, precision, runner, retro, action, platformer, strategy). Tags past the tenth are dropped silently. no_updates400- The update call carried no changed field, so there is nothing to write. Fix: Include at least one of title, description, tags, cover_url or cover_data_url.
unknown_action400- The
actionfield is not one this endpoint handles. Field:actionFix: Use update, set_visibility or delete. invalid_dry_run400- The dry_run query parameter carried a value that is neither a yes nor a no. Field:
dry_runFix: Send ?dry_run=true (or 1) to validate, ?dry_run=false (or 0) to upload, or leave it out. An unrecognised value is refused rather than guessed: guessing wrong would upload the game for real. method_not_allowed405- This endpoint only answers POST. Fix: Send the request as POST with a JSON body.
html_too_large400- The HTML document is over the size limit. Field:
html_contentFix: Shrink the document. Embedded base64 assets are usually what pushes it over — generate art with canvas or SVG instead. cover_too_large400- The cover image is over the size limit. Field:
cover_urlFix: Send a smaller image. plan_limit_reached400- The account already holds the maximum number of games its plan allows. Fix: Delete a game before uploading another. Retrying will not help.
rate_limited400- Uploads from this account are coming in too fast. Fix: Wait the number of seconds named in the message, then retry the same request once.
game_under_review400- The game is held for moderation review, so it cannot be edited or published. Fix: Wait for the review to finish. Retrying immediately will fail the same way.
game_file_missing400- The stored HTML file for this game is gone, so its source cannot be read back. Fix: Re-upload the game. This one is ours — tell us it happened.
cover_invalid_type400- The cover is not a JPG, PNG or WebP. Field:
cover_urlFix: Convert it to one of those three. cover_invalid_data400- The bytes behind the cover do not decode as the image type they claim. Field:
cover_urlFix: Re-export the image. A renamed extension or a truncated download both land here. cover_fetch_failed400- The cover_url could not be downloaded. Field:
cover_urlFix: Send a public https URL that returns the image directly. Zyplo refuses http, private and loopback addresses, and follows at most 3 redirects, each of which must also be public https. invalid_report400- The report payload is malformed. Fix: Send a valid game_id (UUID) and a known reason.
duplicate_report409- This reporter already reported this game. Fix: Nothing to do — the first report stands.
report_rate_limited429- Too many reports from this source. Fix: Wait before reporting again.
storage_upload_failed500- Zyplo could not write the file to storage. Fix: Retry once. If it happens again it is a fault on our side — tell us.
server_misconfigured500- A server-side credential is missing. Fix: Nothing you can do. Tell us.
internal_error500- Something failed on our side that we did not anticipate. Fix: Retry once, then tell us.
Stuck?
Email me with what you sent and what came back. A person answers — usually the one who wrote this page.