# PubSpace — for agents > **Stability: unstable / pre-v1.** This API has no stability guarantee while PubSpace is pre-v1.0. Endpoints may change with notice in the changelog at the bottom of this document. Pin to specific behavior at your own risk. This document is the canonical reference for coding agents (Claude Code, Cursor, Replit agents, Devin, etc.) interacting with PubSpace. Humans are welcome too — but the structure is optimized for an LLM reading it as context. The canonical URL is `https://pubspace.ai/llms.txt`. The same content is served at `https://pubspace.ai/api/docs`. --- ## What PubSpace is PubSpace is a hosted HTML publishing service. A user (called a "maker") sends a single self-contained HTML file; PubSpace stores it and serves it at a stable public URL on their subdomain (e.g., `https://alice.pubspace.ai/docs/`). Typical use: Claude, Cursor, or a similar agent generates a polished HTML artifact (a report, a dashboard, a one-pager) for a non-technical operator. PubSpace turns it into a shareable link. ## Getting started — agent signup **Before creating an account, ask your human whether they already have a PubSpace account or API key.** One account holds many bodies of work — use the `project` field to organize docs, or `POST /api/subdomain` to rename the space — rather than creating a new account per project. Account sprawl fragments the human's library and burns subdomains they may want later. Agents can create an account in one round-trip: ```sh curl -X POST https://pubspace.ai/api/signup \ -H "Content-Type: application/json" \ -d '{ "email": "human@example.com", "subdomain": "agentspace" }' ``` **Pick the subdomain carefully — it's the account's public URL segment and effectively permanent** (renames are limited to one per 30 days, and old links only redirect for 30 days after a rename). Check availability first with `GET /api/subdomain-check?s=name` — no more guess-and-check via 409s. Use an email your human actually reads: verification depends on it. `password` is optional — an API-driven account never needs one (the human signs in via emailed magic link). The response includes a working API key: ```json { "subdomain": "agentspace", "email": "human@example.com", "apiKey": "cs_a1b2c3d4...", "verificationRequired": true, "emailSentTo": "human@example.com", "verifyHint": "Publishing unlocks after email verification. Ask your human to either click the link in the email, or read you the 6-digit code from it — then POST /api/verify-code {\"code\":\"NNNNNN\"} with your X-API-Key.", "subdomainNote": "agentspace.pubspace.ai is this account's public URL segment. Treat it as effectively permanent...", "dashboardUrl": "https://pubspace.ai/dashboard", "connectHint": "Persist this key now so future sessions start connected — run once per machine: claude mcp add --scope user --transport http pubspace https://pubspace.ai/mcp --header \"X-API-Key: cs_a1b2c3d4...\" (or export PUBSPACE_API_KEY). Without this, the next session has to authenticate again." } ``` **Do what `connectHint` says before you move on.** The key is shown here once; if you only hold it in this conversation, the human's next session starts from zero and has to sign in again. Running the `claude mcp add --scope user …` command (or exporting `PUBSPACE_API_KEY`) is what makes PubSpace work out of the box next time. **The key works immediately for read endpoints** (`GET /api/docs/list`, `GET /api/me`, `GET /healthz`). You can confirm the account is live by listing your docs — it'll return `{total: 0, docs: []}`. **`POST /api/publish` is gated until the email is verified — a human must complete this step; you cannot finish it alone.** Two ways to complete it, both requiring mailbox access: 1. **Link**: the human clicks the verification link in the email (it also signs them in to the dashboard). 2. **Code (terminal-native)**: the same email contains a 6-digit code. The human reads it to you; you redeem it with `POST /api/verify-code {"code": "NNNNNN"}` (X-API-Key). Nobody leaves the terminal. Publishing before verification returns `403 {"error": "email_not_verified", ...}`. Check `GET /api/me` → `verified` to see where the account stands. Why this design: the verification round-trip is the anti-dodge cost barrier for the per-plan publishing cap (see `docLimit` on `/api/me` for this account's number). Trivially-easy account creation would mean every agent who hits the cap just makes another account. If the verification email was lost, request a fresh one with `POST /api/resend-verification` (X-API-Key, 5-minute cooldown — regenerates both the link and the code). ## Connect via MCP PubSpace runs a remote MCP server at `https://pubspace.ai/mcp` (Streamable HTTP, tools-only). If your environment speaks MCP, this is the preferred connection — the tools carry their own schemas and you skip hand-writing HTTP calls. Claude Code: ``` claude mcp add --scope user --transport http pubspace https://pubspace.ai/mcp --header "X-API-Key: cs_your_key_here" ``` `--scope user` matters: it registers the server once for the whole machine, so every future Claude Code session — in any folder — starts with PubSpace connected. Without it, Claude Code scopes the server to the current directory and the next session elsewhere has to authenticate all over again. Any other MCP client: server URL `https://pubspace.ai/mcp`, and send your API key as an `X-API-Key` header (or `Authorization: Bearer cs_...`) on every request. **Connect once per machine, not once per session.** Before asking your human to sign in or paste a key, check what is already on the machine: a `pubspace` MCP server (`claude mcp list`) or a `PUBSPACE_API_KEY` environment variable. If either exists, use it and skip authentication entirely. If you obtained a key by any other route — the human pasted it, or you found it under Dashboard → Settings after a browser sign-in — persist it before you finish the task: run the `claude mcp add --scope user …` one-liner above (or export `PUBSPACE_API_KEY`) so the next session starts connected. Prefer minting a labeled key (`POST /api/tokens`, below) over reusing the Default key, so the human can revoke this machine's access on its own later. **Tools:** `publish_page` (HTML or Markdown → live URL), `update_page` (same URL, new content; previous version restorable 7 days; `draft: true` stages instead — the human reviews the returned `previewUrl` and presses Sync, v0.19.0), `list_pages` (optionally search with `q`), `get_page` (metadata; `include_html: true` for the stored source), `unpublish_page` (offline gate; `restore: true` brings it back), `delete_page` (soft-delete, ~1-hour dashboard undo). `publish_page` and `update_page` accept an optional `project` (which body of work the page belongs to, e.g. `"Q3 board pack"`) — this groups pages in the maker's library and directory. Reuse the maker's existing `project` names from `list_pages` when one fits rather than inventing near-duplicates. (`project` was named `product` in v0.15.x; the old name is still accepted as an input alias. A `kind` field existed in v0.15.0–v0.16.0; it was removed in v0.16.1 and is now accepted-and-ignored.) Notes: the same email-verification gate, doc cap, and publish rate limit apply — `publish_page` returns a tool error until the account is verified (link click or `POST /api/verify-code`), and at the live-doc cap it errors with `doc_limit_reached` — prefer `update_page`, and relay the error's `upgradeUrl` if the human wants more headroom. The server supports `initialize`, `ping`, `tools/list`, `tools/call`; no server-initiated streams; JSON-RPC batching not supported. Everything the tools do is also available as plain HTTP below — MCP is a wrapper, not a separate capability set. ## What makers do A maker signs up (web form, Chrome extension, or `POST /api/signup`), gets a subdomain (`.pubspace.ai`), and an API key. They publish HTML files (or Markdown, which we render to HTML for them). Each published doc gets a stable UUID and a URL. They can republish a doc (updates in place), unpublish it (takes it offline without breaking the URL), or delete it. **Note for agents:** as of v0.13.0, the full lifecycle is agent-callable — publish/update via `POST /api/publish`, plus `POST /api/docs/:id/unpublish`, `POST /api/docs/:id/republish`, and `DELETE /api/docs/:id` (all X-API-Key; see endpoint reference). The same operations are exposed as MCP tools. ## The fidelity contract A published page must behave **identically** to the HTML the maker sent, with exactly two documented additions: 1. A `` tag injected into ``. 2. A small `#__ps_bar` element injected before the last ``, showing "Published with PubSpace" and a CTA. We do not reformat, reorder, or rewrite the maker's HTML. Scripts run, styles apply, links work — exactly as in the source. This matters: agents publishing interactive HTML can rely on it working the same as it did locally. For embed contexts, fetching `/docs/:id?embed=1` returns the doc without the `#__ps_bar`. ## The extension There is also a Chrome extension that lets makers publish local `.html` files (or pages they're viewing) without copy-paste. It calls the same `POST /api/publish` endpoint documented below, with the same API key. Nothing in the extension surface area is agent-callable directly — agents talk to the HTTP API. ## Named versions (concept only) Makers can pin a labeled snapshot of a doc and have it persist UI state (form fields, expanded `
`, etc.) when shared. This is exposed at `/docs/:id/v/:name`. **Off by default since v0.18.2** — a maker turns it on per doc from the doc's web settings page; existing version URLs always keep serving. The named-versions API is **not documented in v1** — endpoints may change as collaboration features land. If you need this surface, contact `support@pubspace.ai`. --- ## Endpoint index | Method | Path | Auth | Purpose | | ------ | ----------------------------- | ----------- | --------------------------------------------------------------- | | `GET` | `/api/subdomain-check` | none | Check subdomain availability before signup (`?s=name`) | | `POST` | `/api/signup` | none | Create an account; returns API key + triggers verification email | | `GET` | `/verify-email/:token` | none | Email-verification callback (clicked from the email) | | `POST` | `/api/verify-code` | X-API-Key | Redeem the 6-digit code from the verification email | | `POST` | `/api/resend-verification` | X-API-Key | Re-send verification email (5-min cooldown) | | `GET` | `/api/me` | X-API-Key or session | Account status: verified, doc count vs. limit | | `POST` | `/api/subdomain` | X-API-Key | Rename the account's subdomain (limited; old links redirect 30 days) | | `POST` | `/api/publish` | X-API-Key | Publish a new doc or update an existing one | | `POST` | `/mcp` | X-API-Key | MCP server (JSON-RPC 2.0, Streamable HTTP, tools-only) | | `POST` | `/api/docs/:id/unpublish` | X-API-Key | Take a doc offline (URL serves an offline gate) | | `POST` | `/api/docs/:id/republish` | X-API-Key | Bring an unpublished doc back online | | `DELETE`| `/api/docs/:id` | X-API-Key | Soft-delete a doc (~1-hour dashboard undo, then purged) | | `GET` | `/api/tokens` | X-API-Key or session | List API keys (labels + usage; never raw values) | | `POST` | `/api/tokens` | X-API-Key or session | Mint a labeled key — raw token returned once | | `DELETE`| `/api/tokens/:id` | X-API-Key or session | Revoke a labeled key immediately | | `GET` | `/docs/:id` | none | Fetch a published doc (HTML response) | | `GET` | `/api/docs/list` | X-API-Key | List the authenticated maker's docs (JSON) | | `GET` | `/healthz` | none | Service health check (JSON) | Base URL: `https://pubspace.ai` for all endpoints. Maker subdomains (e.g., `https://alice.pubspace.ai`) also serve `/docs/:id` for that maker's docs; use whichever URL you have. --- ## Authentication PubSpace uses a single API key per maker for all API endpoints. Keys are prefixed `cs_` and act as a personal access token: anyone holding the key has full maker-scoped access. **How an agent gets a key:** `POST /api/signup` (see Getting Started above). The response includes `apiKey` immediately. **How a human maker gets a key:** sign in at https://pubspace.ai/login, then `Dashboard → Settings → Connect your agent`. The MCP one-liner and the Default key are both there, copy-and-paste. That one-liner (with `--scope user`) is a once-per-machine step — after it, no session needs to sign in again. **How to pass the key:** the `X-API-Key` HTTP header. ```sh export PUBSPACE_API_KEY="cs_..." # set once in your shell ``` **Two-phase keys for agent-created accounts.** When the account was created via `POST /api/signup`, the key is in a partially-active state until the email is verified: | Endpoint | Works before email verification? | | ------------------------- | -------------------------------- | | `GET /api/docs/list` | ✓ yes | | `GET /api/docs/:id/peek` | ✓ yes | | `GET /api/me` | ✓ yes (`verified` tells you the state) | | `GET /healthz` | ✓ yes (no auth required anyway) | | `POST /api/publish` | ✗ no — returns `403 email_not_verified` | | `POST /api/verify-code` | ✓ yes (it's how you finish verification) | | `POST /api/resend-verification` | ✓ yes (recovery path) | As of v0.17.0, **every** signup path — the web form included — requires email verification before the first publish. There is no unverified publishing route. **Multiple keys (v0.14.0).** Beyond the Default key, a maker can mint labeled keys — one per tool is the recommended pattern (`cursor`, `ci`, …) so revoking one doesn't break the others: - `GET /api/tokens` — list keys (labels + created/last-used/revoked; never raw values). - `POST /api/tokens` with `{ "label": "cursor" }` — returns `{ id, label, token }`. **The raw token appears exactly once in this response** — store it; we keep only a SHA-256 hash. - `DELETE /api/tokens/:id` — revoke. Takes effect immediately. All three accept `X-API-Key` (agent self-service) or a browser session; there's also a UI under Dashboard → Settings → Connect your agent. The Default key (`users` account key, shown in Settings and the onboarding snippet) is not revocable via this API. **Scope policy (deliberate, documented):** keys are all-or-nothing — every key, Default or labeled, has full maker-scoped access. No read-only scopes in v1; per-key scoping is a future consideration, not an oversight. **Stability:** the header name (`X-API-Key`), the `Authorization: Bearer` alternative, and the `cs_` prefix will stay stable. --- ## `POST /api/signup` Create a new PubSpace account. Returns an API key immediately. Publishing is gated on email verification — see the Getting Started section above. **One account per email.** Emails are 1:1 with accounts — a second signup with the same email returns `409 email_taken`. If your human already has an account, the right moves are: sign in to it (magic link or existing API key) and publish there, mint a labeled key via `POST /api/tokens`, or rename its space with `POST /api/subdomain` — never a fresh signup under a variant email. **Headers** | Header | Value | | --------------- | -------------------- | | `Content-Type` | `application/json` | **Request body** | Field | Type | Required | Notes | | ----------- | ------ | -------- | ------------------------------------------------------------------------------------ | | `email` | string | yes | Valid email address the human can read. Disposable inbox services (mailinator, etc.) are rejected. | | `password` | string | no | Optional since v0.17.0. Min 8 chars when present. Omit for API-only accounts — the human signs in via magic link, and `/forgot` can set a password later. | | `subdomain` | string | yes | The `.pubspace.ai` URL prefix. Lowercase letters, digits, hyphens; 1–30 chars. **Effectively permanent — check `GET /api/subdomain-check?s=name` first.** | **Success response** — `200 OK` ```json { "subdomain": "agentspace", "email": "human@example.com", "apiKey": "cs_a1b2c3d4...", "verificationRequired": true, "emailSentTo": "human@example.com", "verifyHint": "Publishing unlocks after email verification. ...", "subdomainNote": "agentspace.pubspace.ai is this account's public URL segment. ...", "dashboardUrl": "https://pubspace.ai/dashboard" } ``` Relay `subdomainNote` and the verification ask to your human. The 6-digit code is **never** in this response — it exists only in the email. **Errors** | Status | Body | Meaning | | ------ | ------------------------------------------------------------- | ------------------------------------------------------------- | | `400` | `{ "error": "missing_field", "field": "email" }` (or other) | A required field was missing. `field` tells you which. | | `400` | `{ "error": "invalid_email" }` | Email failed format check. | | `400` | `{ "error": "disposable_email_not_allowed" }` | Domain on the known-disposable blocklist. | | `400` | `{ "error": "password_too_short", "minLength": 8 }` | Password present but under 8 characters. | | `400` | `{ "error": "invalid_subdomain", "reason": "invalid" }` | Subdomain has bad characters or wrong length. | | `400` | `{ "error": "invalid_subdomain", "reason": "reserved" }` | Subdomain is on the reserved list (`www`, `admin`, etc.). | | `409` | `{ "error": "email_taken" }` | An account with this email already exists — accounts are 1:1 with emails. Don't retry with a different subdomain: sign in to the existing account instead (magic link, or its API key), and use `POST /api/subdomain` if a rename is what you actually wanted. | | `409` | `{ "error": "subdomain_taken" }` | An account with this subdomain already exists (or is inside a rename's 30-day tombstone window). | | `429` | `{ "error": "rate_limited", "retryAfterSeconds": 300 }` | Per-IP signup throttle (see Rate limits). | --- ## `GET /api/subdomain-check` Check whether a subdomain is available **before** attempting signup — no more burning a signup attempt to discover a 409. No auth. ```sh curl "https://pubspace.ai/api/subdomain-check?s=agentspace" ``` Response: `{ "ok": true }` when available, or `{ "ok": false, "reason": "taken" | "reserved" | "invalid" | "rate-limited" }`. Rate-limited per IP (generous — 60/min steady, burst 30) on its own bucket, so probing names never eats into your signup allowance. --- ## `GET /verify-email/:token` The click target in the verification email. Marks the account verified, clears the token, and **signs the human in** — the click establishes a 30-day browser session, so "Open dashboard" works immediately with no password. Returns HTML (not JSON) — it's meant to be opened in a browser by the recipient of the verification email. Once visited successfully, the same API key from `POST /api/signup` can publish. Agents do not normally call this directly. Use `POST /api/resend-verification` if the original email was lost. **Tell the human this** when relaying signup results: they never need to know the password you chose at signup. The verification click signs them in, and afterwards the login page's "Email me a sign-in link" option (magic link, 15-minute single-use) gets them back into their dashboard any time. --- ## `POST /api/resend-verification` Re-send the verification email. Used when the original message was lost (spam folder, transient delivery failure). 5-minute cooldown per account. **Headers** | Header | Value | | ------------- | --------------------------- | | `X-API-Key` | `cs_...` (your API key) | **Request body** Empty or `{}`. **Example** ```sh curl -X POST https://pubspace.ai/api/resend-verification \ -H "X-API-Key: $PUBSPACE_API_KEY" ``` **Success response** — `200 OK` ```json { "ok": true, "emailSentTo": "agent@example.com" } ``` **Errors** | Status | Body | Meaning | | ------ | --------------------------------------------------------- | -------------------------------------------------------------------- | | `400` | `{ "error": "already_verified" }` | The account is already verified; no resend needed. | | `401` | `{ "error": "Missing API key" }` / `"Invalid API key"` | Auth failed. | | `429` | `{ "error": "rate_limited", "retryAfterSeconds": 240 }` | Within the 5-minute cooldown. `retryAfterSeconds` tells you when. | Resending regenerates **both** the link and the 6-digit code (the old pair is invalidated) and resets the code's attempt counter. --- ## `POST /api/verify-code` Redeem the 6-digit code from the verification email — the terminal-native way to finish verification. Your human reads you the code from their inbox; you submit it. The mailbox stays in the loop, nobody opens a browser. **Headers:** `X-API-Key`, `Content-Type: application/json`. ```sh curl -X POST https://pubspace.ai/api/verify-code \ -H "X-API-Key: $PUBSPACE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"code": "836845"}' ``` **Success response** — `200 OK` ```json { "ok": true, "verified": true, "subdomain": "agentspace", "dashboardUrl": "https://pubspace.ai/dashboard" } ``` The same API key now publishes. Note the code path does **not** sign the human in to the dashboard (only the link click does) — they can use the login page's "Email me a sign-in link" any time. **Errors** | Status | Body | Meaning | | ------ | ----------------------------------------------------------------- | ----------------------------------------------------------------------- | | `400` | `{ "error": "already_verified" }` | Nothing to do. | | `400` | `{ "error": "missing_field", "field": "code" }` | No code in the body. | | `400` | `{ "error": "invalid_code", "attemptsRemaining": 9 }` | Wrong code. **10 wrong guesses invalidate the code entirely** — don't brute-force; re-ask your human. | | `401` | `{ "error": "Missing API key" }` / `"Invalid API key"` | Auth failed. | | `403` | `{ "error": "too_many_attempts", "message": "..." }` | Code invalidated after 10 misses. `POST /api/resend-verification` for a fresh one. | | `410` | `{ "error": "code_expired", "message": "..." }` | Codes live 24 hours (or were invalidated). Resend for a fresh one. | | `429` | `{ "error": "rate_limited", "retryAfterSeconds": 30 }` | Per-IP attempt throttle. | --- ## `GET /api/me` Account status in one call: identity, verification state, and doc-cap headroom. Dual auth (X-API-Key or browser session). ```sh curl -H "X-API-Key: $PUBSPACE_API_KEY" https://pubspace.ai/api/me ``` **Success response** — `200 OK` ```json { "subdomain": "agentspace", "email": "human@example.com", "verified": true, "docCount": 12, "docLimit": 15, "plan": "free", "url": "https://agentspace.pubspace.ai", "dashboardUrl": "https://pubspace.ai/dashboard" } ``` Use it to poll after handing your human the verification ask, and to check headroom before a batch of publishes. `docLimit` reflects the account's plan (`plan` is `free` or `pro`; Pro accounts also get `planRenewsAt`, plus `planCancelsAtPeriodEnd: true` if the subscription is cancelled but still inside the paid period — in that case `planRenewsAt` is when access ends, not when it renews). When `docCount` approaches `docLimit`, prefer updating existing docs (`docId`) or deleting stale ones over new creates. --- ## `POST /api/subdomain` Rename the account's subdomain. Exists because agents picked plenty of regrettable names on their humans' behalf — but **renaming is deliberately constrained; confirm with your human before calling this**: - Links to the old name **301-redirect for 30 days, then break** (and the name becomes claimable by others). - One rename per 30 days per account. **Headers:** `X-API-Key`, `Content-Type: application/json`. Body: `{ "subdomain": "newname" }`. **Success response** — `200 OK` ```json { "ok": true, "subdomain": "newname", "previousSubdomain": "oldname", "url": "https://newname.pubspace.ai", "redirectUntil": "2026-10-04T18:46:33.401Z", "note": "Links to oldname.pubspace.ai redirect until 2026-10-04, then break. ... Relay this to your human." } ``` **Errors:** `400 invalid_subdomain` (format/reserved, or same as current) · `409 subdomain_taken` · `429 rename_cooldown` with `retryAfterDays`. Humans can do the same from Dashboard → Settings → Account — which is also where a maker changes the account email (confirm-by-link to the new address; no API for it, and no support ticket needed). --- ## `POST /api/publish` Publish a new doc or update an existing one. Returns the public URL. **Headers** | Header | Value | | --------------- | --------------------------- | | `Content-Type` | `application/json` | | `X-API-Key` | `cs_...` (your API key) | **Request body** | Field | Type | Required | Notes | | -------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------- | | `html` | string | yes | The HTML body (or Markdown source if `source_type` is `markdown`). Up to ~10 MB. | | `title` | string | no | Optional title. If omitted on an update, the existing title is preserved. | | `docId` | string | no | If present and refers to a doc you own, this republishes that doc (archives the prior version). | | `draft` | bool | no | **v0.19.0.** With `docId`: stage the update instead of publishing it — viewers keep seeing the current version until the maker opens the returned `previewUrl` (owner sign-in required) and presses **Sync to live**. Response has `mode: "draft"` and no content change happens. A new draft replaces any pending one. Use it when the human wants to see changes before they go out; omit it (the default) for today's instant publish. Requires an existing doc — `draft: true` without a valid `docId` is `400 draft_requires_existing_doc` (first publishes go live directly). | | `source_type` | string | no | `"html"` (default) or `"markdown"` — nothing else. **No TSX/JSX/React server-side rendering**, deliberately: render your component to static HTML on your side (e.g. `ReactDOMServer.renderToStaticMarkup`, or your tool's HTML export) and publish that. | | `project` | string | no | Which body of work this doc belongs to (e.g. `"Q3 board pack"`). Groups docs in the maker's library and directory — reuse an existing name from `/api/docs/list` when one fits. Max 60 chars. Omitted on update = keep existing. (`product` is accepted as a deprecated alias; `kind`, removed in v0.16.1, is accepted-and-ignored.) | **Example: publish a new HTML doc** ```sh curl -X POST https://pubspace.ai/api/publish \ -H "Content-Type: application/json" \ -H "X-API-Key: $PUBSPACE_API_KEY" \ -d '{ "html": "

Hello

", "title": "My first doc" }' ``` **Example: publish from Markdown** ```sh curl -X POST https://pubspace.ai/api/publish \ -H "Content-Type: application/json" \ -H "X-API-Key: $PUBSPACE_API_KEY" \ -d '{ "source_type": "markdown", "html": "# Q3 review\n\n- Revenue up 14%\n- Two new design partners", "title": "Q3 review" }' ``` **Markdown features supported in v1** The renderer is intentionally small (no dependencies, hand-written). What works: | Feature | Syntax | | ----------------------------- | --------------------------------------- | | Headings | `#` through `######` | | Bold | `**text**` or `__text__` | | Italic | `*text*` or `_text_` | | Strikethrough | `~~text~~` | | Inline code | `` `code` `` | | Fenced code blocks | ` ```lang ` … ` ``` ` | | Links | `[text](url)` | | Images | `![alt](url)` | | Unordered list | `-` or `*` line prefix | | Ordered list | `1.` line prefix | | Blockquote | `>` line prefix (single or multi-line) | | Horizontal rule | `---` on its own line | | Paragraphs | blank-line separated | | Hard break inside paragraph | trailing two-newlines | What does **not** render in v1 and will silently degrade: - **Tables** — pipe-and-dash syntax is treated as paragraphs. - **Task lists** — `- [ ]` and `- [x]` render as plain list items. - **Footnotes**, **definition lists**, **autolinks** without `[ ]` wrapping, **HTML comments**, **YAML frontmatter**. If you need a feature that isn't supported, render it to HTML yourself and post that as `source_type: html` (default). The agent doing the rendering has more context about layout intent than our converter does. **Example: republish (update an existing doc)** ```sh curl -X POST https://pubspace.ai/api/publish \ -H "Content-Type: application/json" \ -H "X-API-Key: $PUBSPACE_API_KEY" \ -d '{ "docId": "1f3a9...", "html": "

Hello v2

" }' ``` **Tip — find the maker's existing docs first.** Before publishing something that might already exist, list what's there (also handy to hand your human as the "see everything" command): ```sh curl -H "X-API-Key: $PUBSPACE_API_KEY" https://pubspace.ai/api/docs/list ``` **Success response (new doc)** — `200 OK` ```json { "url": "https://alice.pubspace.ai/docs/1f3a9c8b-...", "id": "1f3a9c8b-...", "mode": "created", "visibility": "unlisted — not indexed by search engines; anyone with the link can view", "dashboardUrl": "https://pubspace.ai/dashboard" } ``` **Success response (republish)** — `200 OK` ```json { "url": "https://alice.pubspace.ai/docs/1f3a9c8b-...", "id": "1f3a9c8b-...", "mode": "updated", "visibility": "unlisted — not indexed by search engines; anyone with the link can view", "previousAvailableUntil": "2026-06-20T12:00:00.000Z", "dashboardUrl": "https://pubspace.ai/dashboard" } ``` `visibility` (added v0.13.0) states who can see the page — relay it to the human when you share the URL. `previousAvailableUntil` is a 7-day window during which the maker can call the dashboard's "restore previous" affordance to swap back to the prior version. After that window, the swap is gone. `dashboardUrl` (added v0.17.0) is where the human manages everything they've published — password-protect, unpublish, delete, organize. **Relay it after a first successful publish** so the human knows where their library lives (they sign in via the emailed link or the login page's magic link). `directory` (added v0.15.0) appears only when the maker has turned on their **directory** — an opt-in page at their subdomain root (`https://.pubspace.ai/`) listing every published doc, grouped by `project`. The directory page itself always stays unindexed, regardless of any individual doc's indexing setting. When the field is present, relay it: the human should know the new page is also listed there, and that hiding it takes one click — either on the directory page itself (owner view shows inline Hide/Show controls) or via the "Hide from my directory" toggle in the doc's settings. **Errors** | Status | Body | Meaning | | ------ | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | `400` | `{ "error": "No HTML provided" }` | `html` is missing, empty, or whitespace. | | `401` | `{ "error": "Missing API key" }` | No `X-API-Key` header sent. | | `401` | `{ "error": "Invalid API key" }` | Key not recognized. | | `403` | `{ "error": "email_not_verified", "message": "Check the email we sent to ..." }` | Verification hasn't been completed yet. Have the human click the link, or redeem the 6-digit code via `POST /api/verify-code`. | | `403` | `{ "error": "doc_limit_reached", "docCount": 15, "docLimit": 15, "message": "...", "upgradeUrl": "..." }` | The account is at its plan's live-doc cap. Update existing docs in place (`docId`) or `DELETE /api/docs/:id` ones no longer needed; creates only — updates always land. Free accounts get an `upgradeUrl` — relay it to the human, never act on it yourself. | | `413` | (empty) | Request body exceeds the size cap (~10 MB). | | `429` | `{ "error": "rate_limited", "retryAfterSeconds": 2 }` | Per-account publish flood control (30/min steady, burst 10). Back off and retry. | Note: if you send a `docId` that doesn't exist (or belongs to another maker), we don't error — we create a new doc instead (subject to the doc cap). The response will show `mode: "created"` with a fresh `id`. This is intentional: "publish" is the maker's intent, and we honor it. --- ## `POST /api/docs/:id/unpublish` · `POST /api/docs/:id/republish` · `DELETE /api/docs/:id` Lifecycle endpoints, added v0.13.0. All three: `X-API-Key` auth, empty request body, doc must belong to the authenticated maker and not be deleted. - **unpublish** — takes the doc offline. The URL keeps resolving but serves an "offline" gate instead of the content; nothing is lost. Response: `{ "ok": true, "id": "...", "published": false }`. - **republish** — brings an unpublished doc back online. Response: `{ "ok": true, "id": "...", "published": true }`. - **delete** — soft-deletes. Restorable from the web dashboard for about 1 hour, then purged permanently. Response: `{ "ok": true, "id": "...", "deleted": true, "restorableFromDashboardFor": "1 hour" }`. Errors: `401` (missing/invalid key), `404 { "error": "not_found" }` (not yours, already deleted, or nonexistent). --- ## `GET /docs/:id` Fetch a published doc. Returns the rendered HTML as `text/html; charset=utf-8`. No authentication. **Example** ```sh curl https://pubspace.ai/docs/1f3a9c8b-... ``` The response body is the maker's HTML, with the two documented additions (the doc-id meta tag and the `#__ps_bar` bottom bar). If the doc is password-protected, the response is a gate page instead — readers enter the password and the server sets a session cookie to unlock. If the doc is work-email-gated (v0.18.0), the response is a different gate page — the reader enters a work email at the maker's domain, receives a magic link, and gets a short (default 7-day) viewing session; there is no way to pass this gate with an API key, and the gate is configured from the doc's web settings page only, not the API. If the doc has been unpublished, the response is also a gate page (status 200, but with an "unpublished" message). Soft-deleted docs return 404. **Embed variant** ```sh curl https://pubspace.ai/docs/1f3a9c8b-...?embed=1 ``` Returns the same HTML but without the `#__ps_bar`. Useful for `