HTML Artifacts
Publish an HTML page, SVG, or Mermaid source to a public Kolbo URL and update it in place.
Publish a self-contained HTML page (or an SVG / Mermaid document) and get back a public, shareable URL. This is what the MCP tool publish_html_artifact uses for one-off deliverables: presentations, landing pages, dashboards, reports.
This endpoint is not under /api/v1. It is a Kolbo app route — POST /api/artifact/quick-share — that also accepts the X-API-Key header, so API-key clients can call it exactly like a /v1 route. There is no /api/v1 equivalent, and its request/response fields are camelCase (allowJs, shareToken), not the snake_case used across /v1.
Endpoint
POST /api/artifact/quick-shareRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Page title, also the base of the SEO slug. Missing or empty → 400 title and content are required. Keep it short; the slug is truncated to 50 characters. |
content | string | Yes | The raw document. For html a full HTML document (DOCTYPE + html/head/body); for svg an <svg> document; for mermaid the Mermaid source text. Missing or empty → 400. |
type | string | No | Default: "html". The MCP tool exposes "html" | "svg" | "mermaid"; the underlying record also accepts "react", "jsx", "tsx", "code". Any other value fails schema validation and returns 500. |
allowJs | boolean | No | Default: false. Stored on the artifact record. It does not gate script execution on the published page — see the CSP callout. What it does change: when it is exactly true (and type is html), <iframe> tags survive sanitization instead of being stripped. This is evaluated per request, including on updates. |
shareToken | string | No | Update a previous publish in place. Lookup is scoped to (shareToken, your user); an unknown or foreign token returns 404 Artifact not found. Omit on first publish. |
sessionId | string | No | Optional Mongo ObjectId of a chat session to associate. A non-ObjectId string fails validation (500). |
messageId | string | No | Optional Mongo ObjectId of a chat message to associate. Same validation caveat. |
Identical content is deduplicated. The server hashes the sanitized content and, on a first publish, returns the existing artifact when that hash already exists among your quick-share artifacts (the lookup is scoped to your user plus the container project described below, so an artifact you published from inside a normal Kolbo project does not collide). That response is 200 with duplicate: true instead of 201, and the URL is unchanged.
html content is sanitized before storage. <object>, <embed>, and <applet> are always stripped; <iframe> is stripped too unless the request sends allowJs: true. <script>, forms, canvas, SVG, media elements, and data-* / aria-* attributes survive. Inline event handlers survive only from a fixed allow-list — onclick, onchange, oninput, onsubmit, onload, onerror, onmouseover, onmouseout, onkeydown, onkeyup, onfocus, onblur — so anything else (pointer, drag, touch, wheel handlers) is removed; bind those with addEventListener inside a <script> instead. svg and mermaid content is stored verbatim, without sanitization.
The request body is capped at 10 MB. content travels as JSON, so a page whose markup plus inlined CSS/JS/fonts/data: images exceeds roughly 10 MB is rejected by the body parser with 413 before the handler runs. Do not base64-encode the document to work around this — it only makes the body larger. Load heavy libraries from the CDNs the CSP allows instead of inlining them.
Examples
Publish a New Page
curl -X POST https://api.kolbo.ai/api/artifact/quick-share \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{
"title": "Q3 Campaign Dashboard",
"type": "html",
"content": "<!DOCTYPE html><html><head><title>Q3</title></head><body><h1>Q3 Campaign</h1></body></html>",
"allowJs": true
}'Response (201):
{
"status": true,
"data": {
"_id": "65f1c8a2e4b0a3c1d9f5e321",
"title": "Q3 Campaign Dashboard",
"type": "html",
"shareToken": "a3f4e2b1c8d90a1b2c3d4e5f60718293",
"shareableSlug": "q3-campaign-dashboard-a3f4e2b1c8d90a1b2c3d4e5f60718293",
"siteUrl": null,
"isShared": true,
"version": 1,
"allowJs": true,
"sharedAt": "2026-07-20T10:30:00Z"
},
"message": "Artifact created and shared successfully",
"duplicate": false
}data is the full artifact record (including content, contentHash, slug, versions, customDomain, createdAt/updatedAt; trimmed in the example above) plus two computed fields:
| Field | Notes |
|---|---|
shareToken | 32-char hex token. Keep it — it is how you update the page later. |
shareableSlug | <slugified-title>-<shareToken>. This is the path segment on the sites host. |
siteUrl | Only non-null when a custom subdomain has been assigned to the artifact in the app and is marked active; it then has the form https://sites.kolbo.ai/<subdomain>. Build the public URL from shareableSlug otherwise. |
duplicate | true when the content hash already existed and no new artifact was created. |
updated | Present on the update path only (true when a new version was written). |
Update in Place
Pass the shareToken from a previous publish. The public URL stays the same and the previous content moves into version history.
curl -X POST https://api.kolbo.ai/api/artifact/quick-share \
-H "X-API-Key: kolbo_live_..." \
-H "Content-Type: application/json" \
-d '{
"title": "Q3 Campaign Dashboard",
"content": "<!DOCTYPE html><html>...updated...</html>",
"shareToken": "a3f4e2b1c8d90a1b2c3d4e5f60718293"
}'Response (200):
{
"status": true,
"data": { "version": 2, "shareToken": "a3f4e2b1c8d90a1b2c3d4e5f60718293", "...": "..." },
"message": "Artifact updated in place",
"duplicate": false,
"updated": true
}On the update path: title is applied only when it is a non-blank string, allowJs only when it is a real boolean, messageId only when truthy, and if the new content hashes identically to the current content nothing is written — you get 200 with duplicate: true, updated: false, and no version bump. sessionId is ignored on this path, and type is not written to the record — the artifact keeps the type it was created with. type does still decide how the new content is treated: omit it and the incoming content is sanitized as HTML, so always resend the same type you published with when updating an svg or mermaid artifact. Likewise, resend allowJs: true on an update if the page relies on <iframe> embeds, or they are stripped from the new version.
Reading the Published Page
Three public surfaces serve the same content. None require authentication.
| URL | Returns |
|---|---|
https://sites.kolbo.ai/<shareableSlug> | The page itself. The host resolves a custom subdomain first, then falls back to the share token embedded in the slug. Unknown slug → a branded 404 page. |
https://api.kolbo.ai/api/shared-artifact-raw/<shareToken> | Raw content as text/html. |
https://api.kolbo.ai/api/shared-artifact/<shareToken> | JSON metadata plus content (and the owner's public profile fields). |
Both /api/shared-artifact* routes accept either the plain token or the full slug-token form. Both resolve the token with an isShared: true filter, so an unknown token and an artifact that has been un-shared in the app produce the same response: 404 with a JSON body — not an HTML page.
Rendered content is always served as text/html, whatever the artifact's type. Both surfaces that return the document itself — shared-artifact-raw and the sites host — send the stored content verbatim under Content-Type: text/html; charset=utf-8. (/api/shared-artifact/<shareToken> is the JSON surface and is unaffected.) An svg document still renders because browsers render inline SVG, but a mermaid artifact is served as its raw source text — the public URL does not render it into a diagram. Publish Mermaid as an html page that embeds the diagram if you need a viewable link.
The two /api/shared-artifact* routes are rate limited to 30 requests/minute per IP. POST /api/artifact/quick-share has no dedicated limiter of its own — only the global safety net of 5,000 requests/minute per API key.
Scripts always run on the published page; allowJs does not change that. Both surfaces that serve the document send a fixed Content-Security-Policy, which allows script-src 'self' 'unsafe-inline' 'unsafe-eval' plus jsDelivr, cdnjs, unpkg, d3js.org, cdn.tailwindcss.com, and Cloudflare Insights. Also enforced: connect-src 'self' + Cloudflare Insights (no arbitrary fetch/XHR), form-action 'none' (no form submissions), object-src 'none', base-uri 'none', frame-ancestors 'self' https://*.kolbo.ai, and images/media from any HTTPS origin. frame-src differs between the two: the sites host allows frames only from YouTube and Vimeo, while shared-artifact-raw also allows 'self' and https://*.kolbo.ai (so one Kolbo artifact can embed another). Remember that <iframe> tags only survive in html artifacts published with allowJs: true.
Slug lookups are cached for 60 seconds. A freshly updated page can serve the previous version to the sites host for up to a minute.
Where Artifacts Live
Quick-share artifacts are attached to a per-user container project named "Kobi Code Artifacts", created on first publish and archived so it stays out of the normal project picker. There is no project_id parameter on this endpoint.
Errors
| Status | Cause |
|---|---|
400 | title or content missing/empty. |
401 | Missing or invalid API key. |
403 | Key minted with scope enforcement and lacking the write (or admin) permission — code API_KEY_READ_ONLY. A USD-billed key, which may only call the developer-console API, gets code USD_KEY_ROUTE_RESTRICTED. Also returned for a restricted or email-unverified account. |
404 | shareToken supplied but no artifact with that token belongs to you. |
413 | Request body over the 10 MB JSON limit. |
500 | Invalid type, non-ObjectId sessionId/messageId, or a storage failure. The body is { "status": false, "error": "..." } (note error, not message). |
MCP Tool
publish_html_artifact wraps this endpoint:
| MCP arg | Sent as |
|---|---|
title | title (trimmed) |
content | content — optional when file_path is given; pass exactly one of the two |
file_path | Not sent. The tool reads the file (absolute local path on local stdio installs, or an https:// URL) and sends its text as content. Capped at 5 MB; local paths are rejected over the remote connector |
type | type — "html" | "svg" | "mermaid", default "html" |
allow_js | allowJs — always sent, as true only for an exact boolean true, otherwise false |
share_token | shareToken (trimmed), omitted when blank |
The tool never sends sessionId or messageId; those are only reachable by calling the endpoint directly.
It returns { url, shareToken, shareableSlug, duplicate, updated, title }. Against production url is the artifact's siteUrl when a custom subdomain is active, otherwise https://sites.kolbo.ai/<shareableSlug>; on non-production hosts it falls back to the shared-artifact-raw URL.