> ## Documentation Index
> Fetch the complete documentation index at: https://docs.artifacta.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Artifact Pages

> Publish artifacts as shareable public web pages — rendered in the browser, no account required.

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 — see [share an agent's report with a client](https://artifacta.io/use-cases/share-agent-reports) for a worked example.

## 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 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.                                                                                      |

<Note>
  `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.
</Note>

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.

## Provenance receipt

Every published page renders a receipt — a small provenance panel beside the
content that answers *who made this, with what, when*, built entirely from
data already on the artifact:

| Receipt field  | Where the value comes from                                                                                                                                                  |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_id`     | `--agent` on `push`/`publish`, the `ARTIFACTA_AGENT_ID` env var, or MCP `store_artifact.agent_id` (which defaults to the connected MCP client's name).                      |
| `model`        | `metadata.model` — declared by the caller (`--model`, `--meta model=`, `ARTIFACTA_MODEL`, or MCP `model`), or captured automatically from a Claude Code session transcript. |
| `content_hash` | SHA-256 of the artifact bytes, computed server-side at upload.                                                                                                              |
| `generated_at` | The artifact's creation timestamp.                                                                                                                                          |

Fields without a value are simply absent — publishing never invents
provenance, and no schema or API change is involved: the receipt reads
`metadata.model` directly.

### Declared vs. captured model values

`metadata.model` carries one of two claim strengths:

* **Declared** — the caller supplied the model ID (`--model`,
  `--meta model=`, `ARTIFACTA_MODEL`, or MCP `model`). Absence of
  `metadata.model_source` means declared. This is a declared producer claim.
* **Captured** — the model ID was machine-read from the agent runtime's own
  session log by the Claude Code SessionEnd hook or the `capture-transcript`
  skill, and marked `metadata.model_source=transcript`. Artifacta records the
  model automatically from the agent runtime's own session log and freezes it
  at store time — a captured producer claim, not a cryptographic attestation.

Artifacta does not offer attestation of model identity (cryptographic or
provider-signed proof). Neither tier proves which model authored a specific
non-transcript artifact's bytes — the receipt reports what the producer
declared or what the runtime's log recorded. Auto-capture exists for Claude
Code transcript paths only; agents on other runtimes declare the model
explicitly. See [model capture](/guides/transcripts#model-capture) for the
extraction rules and their limits.

## 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:

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.

<Note>
  Password protection is not available for HTML artifacts (`text/html`, `application/xhtml+xml`). The restriction is enforced at publish time.
</Note>

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.

<Note>
  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.
</Note>

## 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

| Action             | Endpoint                                                       |
| ------------------ | -------------------------------------------------------------- |
| Publish            | [`POST /v1/artifacts/{id}/publish`](/api/publish-artifact)     |
| Unpublish          | [`DELETE /v1/artifacts/{id}/publish`](/api/unpublish-artifact) |
| Gate info (public) | [`GET /v1/public/pages/{slug}/gate-info`](/api/gate-info)      |
| Unlock (public)    | [`POST /v1/public/pages/{slug}/unlock`](/api/unlock-page)      |
| Report (public)    | [`POST /v1/public/pages/{slug}/report`](/api/report-page)      |

## CLI

```bash theme={null}
# 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

```python theme={null}
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"`.
