Artifact Pages let you publish any stored artifact as a web page at artifacta.io/a/{slug}. The page is viewable by anyone with the URL — no Artifacta account required. Agents can publish a build report, a rendered chart, or a Markdown summary and hand the URL to a human in the same step.
How it works
- Upload an artifact with
POST /v1/artifacts (or artifacta push, or store_artifact via MCP).
- Publish it with
POST /v1/artifacts/{id}/publish. The API mints a stable page_id (pg_...) and returns a public_url.
- The viewer at
artifacta.io/a/{slug} fetches page metadata via the get_public_page RPC (anon key, no tenant data exposed) and renders the content.
- Unpublish at any time with
DELETE /v1/artifacts/{id}/publish. The URL stops resolving immediately. The artifact itself is unaffected.
The page_id (and therefore the URL) is stable across re-publishes. Calling publish again on the same artifact updates the title, visibility, and access mode without changing the URL.
Content types and rendering
The viewer selects a renderer based on the artifact’s MIME type:
| Content type | Renderer | Notes |
|---|
text/html | Sandboxed iframe | sandbox="allow-scripts" — no allow-same-origin. Scripts run in a separate origin (artifactausercontent.com). |
text/markdown, text/x-markdown | Markdown | Rendered with react-markdown + rehype-sanitize. Inline images allowed (HTTPS and data: only). Links restricted to https: and mailto:. |
image/* | <img> | SVG served as bytes, never inlined into the page. |
| Anything else | Download card | A styled card with a download button. No inline rendering. |
application/xhtml+xml is treated as HTML for gating — password protection is rejected for it at publish time, same as text/html — but it currently renders as a download card, not in the iframe (only text/html routes to the iframe). Markdown is also matched by the .md/.markdown filename extension, so an artifact stored as text/plain or application/octet-stream with one of those extensions still uses the Markdown renderer.
Common image formats render inline via <img>, including PNG, JPEG (and .jfif), GIF, WebP, SVG, and APNG. .webp, .jfif, and .apng are stored with the correct image/* type by a server-side content-type override even on hosts whose MIME database omits them.
Content origin isolation
Artifact bytes are never served from artifacta.io directly. They are served from a separate registrable domain:
https://artifactausercontent.com/c/{slug}
This is a Cloudflare Worker route. Serving bytes from a separate origin means:
- Untrusted HTML or scripts in an artifact cannot access
artifacta.io cookies or session data.
- The iframe
sandbox attribute provides a second layer: no allow-same-origin means the framed document cannot escalate past its sandbox even if the content-origin were compromised.
- The Worker sets strict
Content-Security-Policy and X-Content-Type-Options: nosniff headers.
Visibility
| Value | Behavior |
|---|
"unlisted" (default) | The page is accessible via URL but is not indexed or listed in any gallery. |
"public" | Reserved for future gallery / search features. Functionally the same as "unlisted" at launch. |
Password protection
Pages can be protected with a passcode (Pro plan). The access flow:
- The viewer calls
GET /v1/public/pages/{slug}/gate-info to check whether a password prompt is needed.
- The visitor submits the password.
- The viewer calls
POST /v1/public/pages/{slug}/unlock with {"password": "..."}.
- The API verifies the passcode (argon2id) and returns a
content_token — a short-lived JWT (5 minutes).
- The viewer passes the token to the content-origin Worker, which serves the gated bytes.
Password protection is not available for HTML artifacts (text/html, application/xhtml+xml). The restriction is enforced at publish time.
Brute-force is mitigated by a per-slug attempt cap on the API plus a per-IP edge rate-limit rule. An incorrect password returns the same unauthorized error as a missing or unpublished slug — the endpoint never reveals whether a slug exists.
After unlock, gated (password-protected) content selects its renderer from the artifact’s stored MIME type only — there is no filename-extension fallback on this path. So, for example, Markdown renders after unlock only when it is stored as text/markdown. The platform ensures this for .md and .markdown uploads via a server-side content-type override, so artifacts uploaded with those extensions render correctly whether or not the page is password-protected.
Abuse reporting
Any visitor can report a page via POST /v1/public/pages/{slug}/report. The endpoint:
- Accepts an optional
reason ("spam", "phishing", "malware", "abuse", "copyright", "other") and a free-text detail.
- Always returns
202 Accepted, regardless of whether the slug exists.
- Forwards the report to PostHog (and optional Slack webhook) for operator review.
- Does not automatically take down the page. Confirmed violations are unpublished by an operator.
Indexing policy
All Artifact Pages are served with noindex at launch. They do not appear in search engine results.
API reference
CLI
# Publish a file
artifacta publish report.pdf --title "Q2 Report" --public
# Unpublish by artifact ID or page slug
artifacta unpublish art_abc123
artifacta unpublish pg_aB3xK9mP1qR5sT2u
Python SDK
from artifacta import Client
client = Client()
# Upload and publish in two steps
artifact = client.push("report.pdf")
page = client.publish_artifact(
artifact.id,
title="Q2 Report",
visibility="public",
)
print(page["public_url"])
# Unpublish
client.unpublish(artifact.id)
MCP (agent usage)
"Upload ./out/report.html and publish it as an unlisted page. Return the URL."
The agent calls store_artifact then publish_artifact. The public_url is available in the tool result immediately.
"Unpublish the page for artifact art_abc123."
The agent calls unpublish_artifact with artifact_id: "art_abc123".