Developer docs

fskk API

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

Publish HTML from your computer

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.

  1. POST file name, byte size, and visibility to /api/uploads/init.
  2. Split the file into 2 MiB chunks, base64 encode each chunk, and POST them in order.
  3. POST the upload token to /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"]))
To use your API key: export FSKK_API_KEY=fsk_…. Keep it private and do not paste it into a prompt or commit it to a repository.

Use fskk in ChatGPT or Claude

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
  • ChatGPT: on the web, open Plugins, choose + → Add custom MCP server, enter a name such as fskk, paste the URL above, choose no authentication for guest publishing, then create it and review the discovered tools. Custom write tools are available on supported Business and Enterprise/Edu workspaces; Pro access is limited. See OpenAI’s current requirements.
  • Claude: open Settings → Connectors → Add custom connector, name it fskk, paste the URL above, and add it. No authentication is needed for guest publishing. Remote MCP connectors are supported on eligible Pro, Max, Team, and Enterprise plans. See Claude’s connector guide.
  • Enable the fskk connector in a chat, attach an .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.
  • The 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.
  • Guest pages are public by URL, expire after 24 hours, and are limited to 10 uploads per hour per IP. A guest page is excluded from the community feed unless you explicitly ask to share it. Private publishing requires an API key.
  • For persistent pages in your account, sign in, create a 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.

Quick start

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.

  1. Sign in and open Settings.
  2. In the API Keys card, give the key a name and choose scopes, then Create API Key.
  3. Copy the key (starting with fsk_) — it is shown only once. Store it somewhere safe.
  4. Send it as a bearer token on every request.

Try it:

curl https://fskk.site/api/v1/pages \
  -H "Authorization: Bearer fsk_your_key_here"
The key is bound to your account and subject to your page limits. Revoke it anytime from Settings.

Base URL & versioning

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.

Authentication

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.

Never share your API key or commit it to source control. It is hashed at rest and cannot be recovered if lost.

Responses

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

Errors return an error object with a stable code:

{ "error": { "code": "NOT_FOUND", "message": "Page not found." } }
HTTPCodeMeaning
400INVALID_REQUESTBody is missing or not a JSON object
400INVALID_HTML_DATAhtml_data is not a JSON object
400INVALID_CUSTOM_URIcustom_uri has invalid characters
401UNAUTHORIZEDMissing or invalid API key
403FORBIDDENKey lacks the required scope
403PAGE_LIMIT_REACHEDPage limit for the account is exhausted
404NOT_FOUNDNo such page
409CUSTOM_URI_TAKENcustom_uri is already in use
429RATE_LIMITEDRate limit reached; wait for Retry-After.

Endpoints

GET /pages

List 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"
POST /pages

Create 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."
  }
}
GET /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"
PATCH /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" }
}
DELETE /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"
GET /pages/{id}/html

Return 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"

The page object

{
  "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"
}

html_data by theme

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.

Notes

  • Pagination: page defaults to 1; per_page defaults to 20 and is capped at 100.
  • Page limits: creating signed-in pages counts against your account's page limit. Guest pages expire after 24 hours and do not need an account.
  • Rate limits: guest HTML uploads allow 10 per hour per IP; authenticated API-key requests allow 300 per minute per IP. A 429 response includes Retry-After.
  • Private pages: the API returns data regardless of the private flag, but the public page stays hidden at /p/….
  • Addressing: {id} accepts the page nanoid or its custom_uri.
  • Rate limits: guest uploads allow 10 per hour per IP; API-key uploads allow 300 requests per minute per IP, including upload initialization, chunks, and completion. The authenticated upload path checks the key on each request and keeps the page in its owner's account.
  • CORS: API reads and writes are allowed cross-origin (no cookies involved), so browser apps work too.
  • Embedding: the /html endpoint returns a full document — ideal for an <iframe srcdoc>.

fskk © 2026