Skip to main content
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

  1. Upload an artifact with POST /v1/artifacts (or artifacta push, or store_artifact via MCP).
  2. Publish it with POST /v1/artifacts/{id}/publish. The API mints a stable page_id (pg_...) and returns a public_url.
  3. 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.
  4. 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 typeRendererNotes
text/htmlSandboxed iframesandbox="allow-scripts" — no allow-same-origin. Scripts run in a separate origin (artifactausercontent.com).
text/markdown, text/x-markdownMarkdownRendered 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 elseDownload cardA 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

ValueBehavior
"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:
  1. The viewer calls GET /v1/public/pages/{slug}/gate-info to check whether a password prompt is needed.
  2. The visitor submits the password.
  3. The viewer calls POST /v1/public/pages/{slug}/unlock with {"password": "..."}.
  4. The API verifies the passcode (argon2id) and returns a content_token — a short-lived JWT (5 minutes).
  5. 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".