HTTP API to publish a web page
One HTTP POST turns AI-generated content into a live HTML web page at a public, shareable URL. Send markdown or structured blocks; get back a link. No API key, no signup — anonymous, rate-limited, and CORS-permissive, so you can call it from curl, a server, a browser, or an AI agent.
Endpoint
POST https://quicky.page/api/v1/publishSend Content-Type: application/json. No API key. No signup. Rate-limited at 30 publishes / 5 minutes / IP. Permissive CORS — call it from curl, a server, a browser, an extension, or an MCP server.
Markdown shape (recommended for agents)
Pass an optional title and a markdown content body. The server converts to qp.v1 blocks for you.
curl -X POST https://quicky.page/api/v1/publish \
-H 'content-type: application/json' \
-d '{
"title": "My AI Summary",
"content": "# Hello\n\nA paragraph with **bold** and a [link](https://example.com).\n\n- one\n- two"
}'Supported markdown subset:
#/##/###headings (deeper collapses to h3)- Paragraphs separated by blank lines
- Unordered (
-,*,+) and ordered (1.) lists **bold**,*italic*,_underline_,`inline code`[label](url)links- Standalone
image lines - Fenced code blocks (
```or~~~, optional language hint) →codeblock. A```mermaidfence renders as a diagram (sequence, flowchart, etc.) on the published page instead of a highlighted block. - Horizontal rules (
---,___,***) →dividerblock - Blockquotes (
> line) →quoteblock - GitHub-style callouts (
> [!NOTE|TIP|IMPORTANT|WARNING|CAUTION]) →calloutblock. The 5 GitHub tones collapse to qp's 3-tone palette:NOTE→ info,TIP→ success,IMPORTANT/WARNING/CAUTION→ warning. - GFM pipe tables (a
| a | b |header row followed by a| --- | :--: |delimiter row) →tableblock, with optional per-column alignment.
Footnotes and inline HTML fall through as plain text — qp.v1's renderer doesn't display them.
Structured shape
Pass an explicit blocks array when you need control over block types (e.g., embeds).
curl -X POST https://quicky.page/api/v1/publish \
-H 'content-type: application/json' \
-d '{
"title": "My AI Summary",
"blocks": [
{ "type": "richtext", "html": "<h2>Subhead</h2><p>Paragraph.</p>" },
{ "type": "image", "url": "https://example.com/diagram.png", "alt": "Diagram" }
]
}'Each block is one of richtext, image, embed, html, callout, quote, divider, timeline, comparison, table, code, or button. All block content is sanitized server-side; unsafe HTML, scripts, and bad URLs are dropped before storage.
qp.v1 schema
The full block discriminated union and the publish payload envelope:
// qp.v1 — block schema for the publish API.
// Pages are a flat, ordered array of these discriminated-union members.
type Block =
| { type: "richtext"; html: string }
| { type: "image"; url: string; alt?: string }
| {
type: "embed";
embedType: "youtube" | "twitter" | "figma" | "codepen" | "iframe_sandbox";
url: string;
title?: string;
}
| {
type: "html";
html: string;
// "strict_sanitized": presentational snippet, scrubbed + injected inline.
// "sandboxed": full HTML document rendered in an isolated sandboxed
// iframe; <style>/layout kept, author JavaScript stripped.
// "pro_sandboxed": same iframe but author JavaScript runs (Pro account
// required at publish time). Never gets allow-same-origin.
safetyMode: "strict_sanitized" | "sandboxed" | "pro_sandboxed";
}
| {
type: "callout";
tone: "info" | "warning" | "success";
html: string;
}
| { type: "quote"; html: string; attribution?: string }
| { type: "divider" }
| {
type: "timeline";
items: Array<{ label: string; body: string }>;
}
| {
type: "comparison";
left: string;
right: string;
leftLabel?: string;
rightLabel?: string;
}
| {
type: "table";
headers: string[]; // column header cells (inline HTML)
rows: string[][]; // body rows; each row matches headers length
align?: Array<"left" | "center" | "right">;
caption?: string;
}
| {
type: "code";
code: string;
language?: "plaintext" | "ts" | "tsx" | "js" | "jsx" | "json" | "html"
| "css" | "py" | "rb" | "go" | "rust" | "java" | "c" | "cpp"
| "csharp" | "swift" | "kotlin" | "php" | "sh" | "sql"
| "yaml" | "toml" | "xml" | "md" | "diff" | "mermaid";
filename?: string;
}
| {
type: "button";
url: string;
label: string;
description?: string;
variant: "filled" | "outline" | "soft";
shape: "rounded" | "pill" | "square";
accent: "theme" | "slate" | "blue" | "green" | "amber"
| "red" | "purple" | "pink";
icon?: string;
};
// Top-level payload when posting blocks directly.
type PageOutput = {
version: "qp.v1";
blocks: Block[];
};Response
{
"id": "abc123",
"url": "https://quicky.page/abc123",
"editUrl": "https://quicky.page/?id=abc123#edit=...", // private edit link; treat as a secret
"editKey": "..." // 24-char raw secret; same key in another form, for programmatic updates
}urlis the public link. Anyone with it can read the page. Safe to share.editUrlis the private edit link in the exact format<base>/?id=<id>#edit=<editKey>— id in the querystring, editKey in the URL fragment (the fragment is never sent to the server, so the secret never appears in access logs or in theRefererheader on outbound clicks). Opens the editor in one click. Anyone with this URL can edit the page — treat as a secret. Give this to the user; do not reconstruct it fromeditKeyyourself.editKeyis the same secret in raw form. Use it as a request-body argument for programmatic updates (POST/api/v1/publishwithid+editKey) or for delete/rename (PATCH/DELETE/api/v1/pages/<id>). There is no recovery if you lose it — just publish a new page.
Updating a page
POST again with id and editKey. The same URL stays live; the new content fully replaces the old. There is no diff, no history, no rollback — Quicky.Page is intentionally not a CMS.
curl -X POST https://quicky.page/api/v1/publish \
-H 'content-type: application/json' \
-d '{
"id": "abc123",
"editKey": "...",
"content": "# New title\n\nUpdated body."
}'Other languages
JavaScript (fetch)
const res = await fetch("https://quicky.page/api/v1/publish", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
title: "My AI Summary",
content: "# Hello\n\nA paragraph.",
}),
});
const { id, url, editUrl, editKey } = await res.json();
console.log(url); // -> https://quicky.page/abc123 (public, share this)
console.log(editUrl); // -> https://quicky.page/?id=abc123#edit=... (private, edit link)Python (requests)
import requests
r = requests.post(
"https://quicky.page/api/v1/publish",
json={
"title": "My AI Summary",
"content": "# Hello\n\nA paragraph.",
},
)
r.raise_for_status()
data = r.json()
print(data["url"]) # -> https://quicky.page/abc123
print(data["editUrl"]) # -> https://quicky.page/?id=abc123#edit=...Errors
| Status | Meaning |
|---|---|
400 | Input couldn't be parsed or no usable content was provided. Body: { "error": "..." }. |
403 | editKey missing or wrong on an update. |
404 | Unknown id on an update. |
429 | Rate limit exceeded. Response includes Retry-After and X-RateLimit-* headers. |
500 | Server error. Retry with backoff. |
Title injection
When you supply title, it becomes a leading <h1>on the page — but only if the body doesn't already start with one. Sending both a title and a markdown content that opens with #won't stack two headings; the explicit one wins.
Reading a page back
GET https://quicky.page/api/v1/pages/<id>Returns the page's content as JSON. Read-only; no editKey required.
Want to publish from inside an AI client? See the MCP server (Claude Desktop, Cursor) or the ChatGPT Custom GPT. Want a one-click publish button in your browser? See the browser extension.
Copy-paste recipes: publish from the terminal, publish AI output to a single URL, share a ChatGPT conversation, and share LLM-generated content.