/pagesList the pages owned by your account, most recently updated first.
Query: ?page=1 & per_page=20 & q=search
curl
curl https://fskk.site/api/v1/pages \
-H "Authorization: Bearer fsk_your_key_here"Developer docs
Publish an HTML page from your computer, ChatGPT, or Claude. Guest publishing needs no login; an API key gives you higher limits and pages saved to your account.
Machine-readable spec: /api/v1/openapi.json
The chunked upload API accepts one .html or .htm file up to 10 MiB.
Guest uploads are public, expire after 24 hours, and are limited to 10 uploads per hour per IP.
The page is not listed in the community feed unless you opt in.
/api/uploads/init./complete and open the returned public URL.For higher API limits and pages kept in your account, sign in, create a key with pages:write, then export it as FSKK_API_KEY before running the script. Authenticated requests allow up to 300 per minute; account page limits still apply. A signed-in key is required for private pages.
Save as publish_html.py, then run python3 publish_html.py ./game.html. Add --show-in-feed to opt a public page into discovery. Add --private with your API key to publish a private page.
#!/usr/bin/env python3
import base64, json, os, pathlib, sys, urllib.error, urllib.parse, urllib.request
BASE = "https://fskk.site"
CHUNK_BYTES = 2 * 1024 * 1024
path = pathlib.Path(sys.argv[1]).expanduser()
show_in_feed = "--show-in-feed" in sys.argv[2:]
is_private = "--private" in sys.argv[2:]
api_key = os.environ.get("FSKK_API_KEY")
file_bytes = path.read_bytes()
if path.suffix.lower() not in (".html", ".htm"):
raise SystemExit("Choose an .html or .htm file.")
if not file_bytes or len(file_bytes) > 10 * 1024 * 1024:
raise SystemExit("The file must be nonempty and no larger than 10 MiB.")
def post(url, value):
headers = {"Content-Type": "application/json"}
if api_key:
headers["Authorization"] = "Bearer " + api_key
request = urllib.request.Request(url, json.dumps(value).encode(), headers, method="POST")
try:
with urllib.request.urlopen(request, timeout=60) as response:
return json.load(response)
except urllib.error.HTTPError as error:
detail = error.read().decode("utf-8", "replace")
if error.code == 429:
detail += "\nGuest limit: 10 uploads/hour. Sign in and set FSKK_API_KEY for higher limits."
raise SystemExit(f"HTTP {error.code}: {detail}")
session = post(BASE + "/api/uploads/init", {
"fileName": path.name,
"totalBytes": len(file_bytes),
"purpose": "page",
"visibility": "private" if is_private else "public",
"showInFeed": show_in_feed and not is_private,
})
for index, start in enumerate(range(0, len(file_bytes), CHUNK_BYTES)):
chunk = file_bytes[start:start + CHUNK_BYTES]
post(BASE + "/api/uploads/" + session["uploadId"] + "/chunk", {
"token": session["token"],
"chunkIndex": index,
"content": base64.b64encode(chunk).decode("ascii"),
})
result = post(BASE + "/api/uploads/" + session["uploadId"] + "/complete", {"token": session["token"]})
print(urllib.parse.urljoin(BASE, result["url"])) export FSKK_API_KEY=fsk_…. Keep it private and do not paste it into a prompt or commit it to a repository.Add fskk as a remote MCP connector. It uses Streamable HTTP and needs no login for guest publishing. MCP publishing creates public pages; use the command-line uploader with a signed-in API key when you need a private page.
https://fskk.site/api/mcp .html or .htm file, and ask: “Publish this HTML file to fskk as a guest. Keep it out of the community feed.” The assistant sends the file contents to fskk; the response contains the public URL.publish_html tool accepts files up to 3 MiB. For files from 3–10 MiB, the assistant uses start_html_upload, upload_html_chunk, then complete_html_upload.pages:write API key, then configure the connector/client to send it as Authorization: Bearer fsk_… if custom bearer headers are supported. Keep the key private. Otherwise, use the guest flow.For a one-off HTML upload, no API key is needed; use the upload flow above. Use an API key when you want to manage pages and keep deployments in your account.
fsk_) — it is shown only once. Store it somewhere safe.Try it:
curl https://fskk.site/api/v1/pages \
-H "Authorization: Bearer fsk_your_key_here"All endpoints live under the versioned prefix:
https://fskk.site/api/v1 Breaking changes will be released under a new version. The current version is v1.
Page management endpoints under /api/v1 use an API key as a bearer token in the Authorization header. HTML upload and MCP publishing also work without a key, with guest limits:
Authorization: Bearer fsk_your_key_here Keys have scopes. pages:read grants list/read endpoints; pages:write grants create, update and delete. A key without the required scope gets 403 Forbidden.
Successful requests return the resource under a data key:
{ "data": { "id": "abc…" } } List endpoints also return meta with pagination:
{ "data": [], "meta": { "page": 1, "perPage": 20, "totalItems": 0, "totalPages": 0 } }Errors return an error object with a stable code:
{ "error": { "code": "NOT_FOUND", "message": "Page not found." } } | HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_REQUEST | Body is missing or not a JSON object |
| 400 | INVALID_HTML_DATA | html_data is not a JSON object |
| 400 | INVALID_CUSTOM_URI | custom_uri has invalid characters |
| 401 | UNAUTHORIZED | Missing or invalid API key |
| 403 | FORBIDDEN | Key lacks the required scope |
| 403 | PAGE_LIMIT_REACHED | Page limit for the account is exhausted |
| 404 | NOT_FOUND | No such page |
| 409 | CUSTOM_URI_TAKEN | custom_uri is already in use |
| 429 | RATE_LIMITED | Rate limit reached; wait for Retry-After. |
/pagesList the pages owned by your account, most recently updated first.
Query: ?page=1 & per_page=20 & q=search
curl
curl https://fskk.site/api/v1/pages \
-H "Authorization: Bearer fsk_your_key_here"/pagesCreate a page. If html_data is omitted, sensible defaults for the theme are used.
curl
curl -X POST https://fskk.site/api/v1/pages \
-H "Authorization: Bearer fsk_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "theme": "default_theme", "custom_uri": "my-links" }'body
{
"theme": "default_theme",
"private": false,
"show_in_feed": false,
"custom_uri": "my-links",
"html_data": {
"title": "My Page",
"subtitle": "All my important links in one place."
}
}/pages/{id}Fetch a single page by nanoid or custom URI.
Path: {id} = page nanoid or custom_uri
curl
curl https://fskk.site/api/v1/pages/{id} \
-H "Authorization: Bearer fsk_your_key_here"/pages/{id}Update a page. Fields and html_data are merged over the current content.
Path: {id} = page nanoid or custom_uri
curl
curl -X PATCH https://fskk.site/api/v1/pages/{id} \
-H "Authorization: Bearer fsk_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "html_data": { "title": "New title" } }'body
{
"theme": "green_theme",
"private": true,
"show_in_feed": false,
"html_data": { "title": "New title" }
}/pages/{id}Permanently delete a page.
Path: {id} = page nanoid or custom_uri
curl
curl -X DELETE https://fskk.site/api/v1/pages/{id} \
-H "Authorization: Bearer fsk_your_key_here"/pages/{id}/htmlReturn the page rendered as a complete HTML document, ideal for <iframe srcdoc>.
Path: {id} = page nanoid or custom_uri
curl
curl https://fskk.site/api/v1/pages/{id}/html \
-H "Authorization: Bearer fsk_your_key_here"{
"id": "abc123…",
"url": "https://fskk.site/p/abc123…",
"custom_uri": "my-links",
"theme": "default_theme",
"private": false,
"html_data": { "title": "My Page", "links": [] },
"is_supporter_page": false,
"expiration_date": null,
"created_at": "2026-09-04T00:00:00.000Z",
"updated_at": "2026-09-04T00:00:00.000Z"
}Template themes (Simple, Purple, Green) store an editor-friendly shape:
{
"title": "My Page",
"subtitle": "All my important links in one place.",
"links": [
{ "id": "link-1", "label": "GitHub", "url": "https://github.com/you", "description": "My code" }
],
"background_color": "#ffffff",
"text_color": "#111827"
} Raw (Custom HTML) pages store HTML and CSS that is rendered as-is:
{ "html": "<h1>Hello World</h1>", "css": "h1 { color: tomato; }" } When updating a page, html_data is merged over the current content, so you only need to send the
fields you change.
page defaults to 1; per_page defaults to 20 and is capped at 100.429 response includes Retry-After.private flag, but the public page stays hidden at /p/….{id} accepts the page nanoid or its custom_uri./html endpoint returns a full document — ideal for an <iframe srcdoc>.