# 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 Agents can create their own PubSpace account in one round-trip and start publishing: ```sh curl -X POST https://pubspace.ai/api/signup \ -H "Content-Type: application/json" \ -d '{ "email": "agent@example.com", "password": "a-strong-password", "subdomain": "agentspace" }' ``` The response includes a working API key: ```json { "subdomain": "agentspace", "email": "agent@example.com", "apiKey": "cs_a1b2c3d4...", "verificationRequired": true, "emailSentTo": "agent@example.com" } ``` **The key works immediately for read endpoints** (`GET /api/docs/list`, `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. PubSpace sends a verification email; the recipient (a human or an inbox the agent can read) clicks the link, and from that moment on the same API key publishes. Publishing before verification returns `403 {"error": "email_not_verified", "message": "Check the email we sent..."}`. Why this design: the verification round-trip is the anti-dodge cost barrier for the future free-tier publishing cap. 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 — see endpoint reference below). ## 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 --transport http pubspace https://pubspace.ai/mcp --header "X-API-Key: cs_your_key_here" ``` 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. **Tools:** `publish_page` (HTML or Markdown → live URL), `update_page` (same URL, new content; previous version restorable 7 days), `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 optional `project` (which body of work the page belongs to, e.g. `"Q3 board pack"`) and `kind` (what sort of asset, e.g. `"report"`, `"dashboard"`, `"memo"`) — these group 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.) Notes: the same email-verification gate applies — `publish_page` returns a tool error until the account's verification link is clicked. 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`. 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 | | ------ | ----------------------------- | ----------- | --------------------------------------------------------------- | | `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/resend-verification` | X-API-Key | Re-send verification email (5-min cooldown) | | `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 → Advanced → API key`. Copy and paste. **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 /healthz` | ✓ yes (no auth required anyway) | | `POST /api/publish` | ✗ no — returns `403 email_not_verified` | | `POST /api/resend-verification` | ✓ yes (recovery path) | Web-form signups (`/signup` at pubspace.ai) auto-verify — humans paid the form-friction cost at signup time. The two-phase key only affects accounts created via the API. **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 → Advanced. 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. **Headers** | Header | Value | | --------------- | -------------------- | | `Content-Type` | `application/json` | **Request body** | Field | Type | Required | Notes | | ----------- | ------ | -------- | ------------------------------------------------------------------------------------ | | `email` | string | yes | Valid email address. Disposable inbox services (mailinator, etc.) are rejected. | | `password` | string | yes | Used if the maker (or you) ever wants to sign in to the web dashboard. | | `subdomain` | string | yes | The `.pubspace.ai` URL prefix. Lowercase letters, digits, hyphens; 1–30 chars. | **Success response** — `200 OK` ```json { "subdomain": "agentspace", "email": "agent@example.com", "apiKey": "cs_a1b2c3d4...", "verificationRequired": true, "emailSentTo": "agent@example.com" } ``` **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": "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. | | `409` | `{ "error": "subdomain_taken" }` | An account with this subdomain already exists. | --- ## `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. | --- ## `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). | | `source_type` | string | no | `"html"` (default) or `"markdown"`. If `"markdown"`, we render it to a styled HTML doc before storing. | | `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` | string | no | What sort of asset this is (e.g. `"report"`, `"dashboard"`, `"memo"`, `"prototype"`). Max 60 chars. Omitted on update = keep existing. | **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

" }' ``` **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" } ``` **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" } ``` `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. `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`, with the same never-indexed trust envelope as individual docs. 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 ..." }` | Account was created via `POST /api/signup` and the verification link hasn't been clicked yet. Click the link or call `POST /api/resend-verification`. | | `413` | (empty) | Request body exceeds the size cap (~10 MB). | 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. 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 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 `